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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/agent_team/task_notification/task_activator.py
|
|
2
|
+
"""
|
|
3
|
+
Defines the component responsible for the action of activating an agent.
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from autobyteus.agent.message import AgentInputUserMessage
|
|
9
|
+
from autobyteus.agent_team.events import ProcessUserMessageEvent
|
|
10
|
+
from autobyteus.agent.sender_type import SenderType, TASK_NOTIFIER_SENDER_ID
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from autobyteus.agent_team.context.team_manager import TeamManager
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
class TaskActivator:
|
|
18
|
+
"""
|
|
19
|
+
A component with the single responsibility of activating an agent.
|
|
20
|
+
|
|
21
|
+
Activation involves ensuring the agent is running and sending it a
|
|
22
|
+
standardized "start work" message.
|
|
23
|
+
"""
|
|
24
|
+
def __init__(self, team_manager: 'TeamManager'):
|
|
25
|
+
"""
|
|
26
|
+
Initializes the TaskActivator.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
team_manager: The team's manager, used to start agents and dispatch messages.
|
|
30
|
+
"""
|
|
31
|
+
if not team_manager:
|
|
32
|
+
raise ValueError("TaskActivator requires a valid TeamManager instance.")
|
|
33
|
+
self._team_manager = team_manager
|
|
34
|
+
logger.debug(f"TaskActivator initialized for team '{self._team_manager.team_id}'.")
|
|
35
|
+
|
|
36
|
+
async def activate_agent(self, agent_name: str):
|
|
37
|
+
"""
|
|
38
|
+
Activates a specific agent by ensuring it is ready and sending it a
|
|
39
|
+
generic "start work" notification.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
agent_name: The unique name of the agent to activate.
|
|
43
|
+
"""
|
|
44
|
+
team_id = self._team_manager.team_id
|
|
45
|
+
try:
|
|
46
|
+
logger.info(f"Team '{team_id}': TaskActivator is activating agent '{agent_name}'.")
|
|
47
|
+
|
|
48
|
+
# This ensures the agent is started and ready to receive the message.
|
|
49
|
+
await self._team_manager.ensure_node_is_ready(agent_name)
|
|
50
|
+
|
|
51
|
+
notification_message = AgentInputUserMessage(
|
|
52
|
+
content="You have new tasks in your queue. Please review your task list using your tools and begin your work.",
|
|
53
|
+
sender_type=SenderType.SYSTEM,
|
|
54
|
+
metadata={'sender_id': TASK_NOTIFIER_SENDER_ID}
|
|
55
|
+
)
|
|
56
|
+
event = ProcessUserMessageEvent(
|
|
57
|
+
user_message=notification_message,
|
|
58
|
+
target_agent_name=agent_name
|
|
59
|
+
)
|
|
60
|
+
await self._team_manager.dispatch_user_message_to_agent(event)
|
|
61
|
+
|
|
62
|
+
logger.info(f"Team '{team_id}': Successfully sent activation notification to '{agent_name}'.")
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
# FIXED: Removed "TaskActivator" from the log message to align with the unit test assertion.
|
|
66
|
+
logger.error(f"Team '{team_id}': Failed to activate agent '{agent_name}': {e}", exc_info=True)
|
|
@@ -16,8 +16,8 @@ from autobyteus.agent.streaming.stream_event_payloads import (
|
|
|
16
16
|
)
|
|
17
17
|
from autobyteus.agent_team.streaming.agent_team_stream_events import AgentTeamStreamEvent
|
|
18
18
|
from autobyteus.agent_team.streaming.agent_team_stream_event_payloads import AgentEventRebroadcastPayload, SubTeamEventRebroadcastPayload, AgentTeamPhaseTransitionData
|
|
19
|
-
from autobyteus.task_management.
|
|
20
|
-
from autobyteus.task_management.events import
|
|
19
|
+
from autobyteus.task_management.task import Task
|
|
20
|
+
from autobyteus.task_management.events import TasksAddedEvent, TaskStatusUpdatedEvent
|
|
21
21
|
from autobyteus.task_management.base_task_board import TaskStatus
|
|
22
22
|
|
|
23
23
|
logger = logging.getLogger(__name__)
|
|
@@ -86,29 +86,26 @@ class TUIStateStore:
|
|
|
86
86
|
self._team_event_history[parent_name].append(event)
|
|
87
87
|
|
|
88
88
|
if event.event_source_type == "TASK_BOARD":
|
|
89
|
-
# The 'parent_name' argument holds the friendly name of the team (or sub-team)
|
|
90
|
-
# that is the context for this event. This is the key we use for UI state.
|
|
91
89
|
team_name_key = parent_name
|
|
92
|
-
if isinstance(event.data,
|
|
93
|
-
self._task_plans[team_name_key] =
|
|
94
|
-
|
|
95
|
-
self.
|
|
96
|
-
|
|
90
|
+
if isinstance(event.data, TasksAddedEvent):
|
|
91
|
+
if team_name_key not in self._task_plans: self._task_plans[team_name_key] = []
|
|
92
|
+
if team_name_key not in self._task_statuses: self._task_statuses[team_name_key] = {}
|
|
93
|
+
self._task_plans[team_name_key].extend(event.data.tasks)
|
|
94
|
+
for task in event.data.tasks:
|
|
95
|
+
self._task_statuses[team_name_key][task.task_id] = TaskStatus.NOT_STARTED
|
|
96
|
+
logger.debug(f"TUI State: Added {len(event.data.tasks)} tasks to board for '{team_name_key}'.")
|
|
97
|
+
|
|
97
98
|
elif isinstance(event.data, TaskStatusUpdatedEvent):
|
|
98
|
-
|
|
99
|
-
if team_name_key not in self._task_statuses:
|
|
100
|
-
self._task_statuses[team_name_key] = {}
|
|
99
|
+
if team_name_key not in self._task_statuses: self._task_statuses[team_name_key] = {}
|
|
101
100
|
self._task_statuses[team_name_key][event.data.task_id] = event.data.new_status
|
|
102
101
|
logger.debug(f"TUI State: Updated status for task '{event.data.task_id}' in team '{team_name_key}' to {event.data.new_status}.")
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
logger.debug(f"TUI State: Synced deliverables for task '{event.data.task_id}' in team '{team_name_key}'.")
|
|
111
|
-
break
|
|
103
|
+
if event.data.deliverables is not None and team_name_key in self._task_plans:
|
|
104
|
+
for task in self._task_plans[team_name_key]:
|
|
105
|
+
if task.task_id == event.data.task_id:
|
|
106
|
+
task.file_deliverables = event.data.deliverables
|
|
107
|
+
logger.debug(f"TUI State: Synced deliverables for task '{event.data.task_id}' in team '{team_name_key}'.")
|
|
108
|
+
break
|
|
112
109
|
return
|
|
113
110
|
|
|
114
111
|
if isinstance(event.data, AgentEventRebroadcastPayload):
|
|
@@ -15,7 +15,7 @@ from textual.containers import VerticalScroll, Horizontal
|
|
|
15
15
|
from autobyteus.agent.phases import AgentOperationalPhase
|
|
16
16
|
from autobyteus.agent_team.phases import AgentTeamOperationalPhase
|
|
17
17
|
from autobyteus.task_management.base_task_board import TaskStatus
|
|
18
|
-
from autobyteus.task_management.
|
|
18
|
+
from autobyteus.task_management.task import Task
|
|
19
19
|
from autobyteus.agent.streaming.stream_events import StreamEvent as AgentStreamEvent, StreamEventType as AgentStreamEventType
|
|
20
20
|
from autobyteus.agent.streaming.stream_event_payloads import (
|
|
21
21
|
AgentOperationalPhaseTransitionData, AssistantChunkData, AssistantCompleteResponseData,
|
|
@@ -6,7 +6,7 @@ from rich.panel import Panel
|
|
|
6
6
|
from rich.text import Text
|
|
7
7
|
from textual.widgets import Static
|
|
8
8
|
|
|
9
|
-
from autobyteus.task_management.
|
|
9
|
+
from autobyteus.task_management.task import Task
|
|
10
10
|
from autobyteus.task_management.base_task_board import TaskStatus
|
|
11
11
|
from .shared import TASK_STATUS_ICONS, LOG_ICON
|
|
12
12
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Client utilities for communicating with the Autobyteus LLM server.
|
|
3
|
+
|
|
4
|
+
Consolidates the previously standalone autobyteus-llm-client package so the
|
|
5
|
+
HTTP client can evolve alongside the rest of the framework.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .autobyteus_client import AutobyteusClient, CertificateError
|
|
9
|
+
|
|
10
|
+
__all__ = ["AutobyteusClient", "CertificateError"]
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, AsyncGenerator, Dict, List, Optional, Union
|
|
6
|
+
from urllib.parse import urljoin
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CertificateError(Exception):
|
|
14
|
+
"""Custom exception for certificate-related errors."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AutobyteusClient:
|
|
18
|
+
"""Async + sync HTTP client for talking to an Autobyteus LLM server."""
|
|
19
|
+
|
|
20
|
+
DEFAULT_SERVER_URL = "https://api.autobyteus.com"
|
|
21
|
+
API_KEY_HEADER = "AUTOBYTEUS_API_KEY"
|
|
22
|
+
API_KEY_ENV_VAR = "AUTOBYTEUS_API_KEY"
|
|
23
|
+
SSL_CERT_FILE_ENV_VAR = "AUTOBYTEUS_SSL_CERT_FILE"
|
|
24
|
+
|
|
25
|
+
def __init__(self, server_url: Optional[str] = None):
|
|
26
|
+
"""
|
|
27
|
+
Initialize the client.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
server_url: Explicit server URL. Takes precedence over env vars.
|
|
31
|
+
"""
|
|
32
|
+
self.server_url = server_url or os.getenv(
|
|
33
|
+
"AUTOBYTEUS_LLM_SERVER_URL", self.DEFAULT_SERVER_URL
|
|
34
|
+
)
|
|
35
|
+
self.api_key = os.getenv(self.API_KEY_ENV_VAR)
|
|
36
|
+
|
|
37
|
+
if not self.api_key:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"{self.API_KEY_ENV_VAR} environment variable is required. "
|
|
40
|
+
"Please set it before initializing the client."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
custom_cert_path_str = os.getenv(self.SSL_CERT_FILE_ENV_VAR)
|
|
44
|
+
verify_param: Union[str, bool, Path]
|
|
45
|
+
|
|
46
|
+
if custom_cert_path_str:
|
|
47
|
+
custom_cert_path = Path(custom_cert_path_str)
|
|
48
|
+
if not custom_cert_path.exists():
|
|
49
|
+
raise CertificateError(
|
|
50
|
+
f"Custom SSL certificate file specified via {self.SSL_CERT_FILE_ENV_VAR} "
|
|
51
|
+
f"not found at: {custom_cert_path}"
|
|
52
|
+
)
|
|
53
|
+
if not custom_cert_path.is_file():
|
|
54
|
+
raise CertificateError(
|
|
55
|
+
f"Custom SSL certificate path specified via {self.SSL_CERT_FILE_ENV_VAR} "
|
|
56
|
+
f"is not a file: {custom_cert_path}"
|
|
57
|
+
)
|
|
58
|
+
verify_param = str(custom_cert_path)
|
|
59
|
+
logger.info(
|
|
60
|
+
"Using custom SSL certificate file for TLS verification: %s. "
|
|
61
|
+
"This is the recommended secure method for servers with self-signed or "
|
|
62
|
+
"private CA certificates.",
|
|
63
|
+
verify_param,
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
verify_param = False
|
|
67
|
+
logger.warning(
|
|
68
|
+
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
|
69
|
+
"SECURITY WARNING: SSL certificate verification is DISABLED because the \n"
|
|
70
|
+
f"'{self.SSL_CERT_FILE_ENV_VAR}' environment variable is not set.\n"
|
|
71
|
+
"This configuration is INSECURE and makes the client vulnerable to \n"
|
|
72
|
+
"Man-in-the-Middle (MitM) attacks. It should ONLY be used for development \n"
|
|
73
|
+
"or testing in trusted environments with self-signed certificates if \n"
|
|
74
|
+
"providing the certificate path is not possible.\n"
|
|
75
|
+
"FOR PRODUCTION or secure environments with self-signed certificates, \n"
|
|
76
|
+
f"it is STRONGLY RECOMMENDED to set the '{self.SSL_CERT_FILE_ENV_VAR}' \n"
|
|
77
|
+
"environment variable to the path of the server's certificate (.pem file) \n"
|
|
78
|
+
"to enable proper TLS verification.\n"
|
|
79
|
+
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
timeout_config = httpx.Timeout(connect=10.0, read=None, write=None, pool=None)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
self.async_client = httpx.AsyncClient(
|
|
86
|
+
verify=verify_param,
|
|
87
|
+
headers={self.API_KEY_HEADER: self.api_key},
|
|
88
|
+
timeout=timeout_config,
|
|
89
|
+
)
|
|
90
|
+
self.sync_client = httpx.Client(
|
|
91
|
+
verify=verify_param,
|
|
92
|
+
headers={self.API_KEY_HEADER: self.api_key},
|
|
93
|
+
timeout=timeout_config,
|
|
94
|
+
)
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
logger.error(
|
|
97
|
+
"Failed to initialize httpx client with SSL configuration (verify='%s'): %s",
|
|
98
|
+
verify_param,
|
|
99
|
+
exc,
|
|
100
|
+
)
|
|
101
|
+
raise RuntimeError(f"HTTP client initialization failed: {exc}") from exc
|
|
102
|
+
|
|
103
|
+
logger.info("Initialized Autobyteus client with server URL: %s", self.server_url)
|
|
104
|
+
|
|
105
|
+
async def get_available_llm_models(self) -> Dict[str, Any]:
|
|
106
|
+
"""Async discovery of available LLM models."""
|
|
107
|
+
try:
|
|
108
|
+
response = await self.async_client.get(
|
|
109
|
+
urljoin(self.server_url, "/models/llm")
|
|
110
|
+
)
|
|
111
|
+
response.raise_for_status()
|
|
112
|
+
return response.json()
|
|
113
|
+
except httpx.HTTPError as exc:
|
|
114
|
+
logger.error("Async LLM model fetch error: %s", exc)
|
|
115
|
+
raise RuntimeError(str(exc)) from exc
|
|
116
|
+
|
|
117
|
+
def get_available_llm_models_sync(self) -> Dict[str, Any]:
|
|
118
|
+
"""Synchronous discovery of available LLM models."""
|
|
119
|
+
try:
|
|
120
|
+
response = self.sync_client.get(urljoin(self.server_url, "/models/llm"))
|
|
121
|
+
response.raise_for_status()
|
|
122
|
+
return response.json()
|
|
123
|
+
except httpx.HTTPError as exc:
|
|
124
|
+
logger.error("Sync LLM model fetch error: %s", exc)
|
|
125
|
+
raise RuntimeError(str(exc)) from exc
|
|
126
|
+
|
|
127
|
+
async def get_available_image_models(self) -> Dict[str, Any]:
|
|
128
|
+
"""Async discovery of available image models."""
|
|
129
|
+
try:
|
|
130
|
+
response = await self.async_client.get(
|
|
131
|
+
urljoin(self.server_url, "/models/image")
|
|
132
|
+
)
|
|
133
|
+
response.raise_for_status()
|
|
134
|
+
return response.json()
|
|
135
|
+
except httpx.HTTPError as exc:
|
|
136
|
+
logger.error("Async image model fetch error: %s", exc)
|
|
137
|
+
raise RuntimeError(str(exc)) from exc
|
|
138
|
+
|
|
139
|
+
def get_available_image_models_sync(self) -> Dict[str, Any]:
|
|
140
|
+
"""Synchronous discovery of available image models."""
|
|
141
|
+
try:
|
|
142
|
+
response = self.sync_client.get(
|
|
143
|
+
urljoin(self.server_url, "/models/image")
|
|
144
|
+
)
|
|
145
|
+
response.raise_for_status()
|
|
146
|
+
return response.json()
|
|
147
|
+
except httpx.HTTPError as exc:
|
|
148
|
+
logger.error("Sync image model fetch error: %s", exc)
|
|
149
|
+
raise RuntimeError(str(exc)) from exc
|
|
150
|
+
|
|
151
|
+
async def get_available_audio_models(self) -> Dict[str, Any]:
|
|
152
|
+
"""Async discovery of available audio models."""
|
|
153
|
+
try:
|
|
154
|
+
response = await self.async_client.get(
|
|
155
|
+
urljoin(self.server_url, "/models/audio")
|
|
156
|
+
)
|
|
157
|
+
response.raise_for_status()
|
|
158
|
+
return response.json()
|
|
159
|
+
except httpx.HTTPError as exc:
|
|
160
|
+
logger.error("Async audio model fetch error: %s", exc)
|
|
161
|
+
raise RuntimeError(str(exc)) from exc
|
|
162
|
+
|
|
163
|
+
def get_available_audio_models_sync(self) -> Dict[str, Any]:
|
|
164
|
+
"""Synchronous discovery of available audio models."""
|
|
165
|
+
try:
|
|
166
|
+
response = self.sync_client.get(
|
|
167
|
+
urljoin(self.server_url, "/models/audio")
|
|
168
|
+
)
|
|
169
|
+
response.raise_for_status()
|
|
170
|
+
return response.json()
|
|
171
|
+
except httpx.HTTPError as exc:
|
|
172
|
+
logger.error("Sync audio model fetch error: %s", exc)
|
|
173
|
+
raise RuntimeError(str(exc)) from exc
|
|
174
|
+
|
|
175
|
+
async def send_message(
|
|
176
|
+
self,
|
|
177
|
+
conversation_id: str,
|
|
178
|
+
model_name: str,
|
|
179
|
+
user_message: str,
|
|
180
|
+
image_urls: Optional[List[str]] = None,
|
|
181
|
+
audio_urls: Optional[List[str]] = None,
|
|
182
|
+
video_urls: Optional[List[str]] = None,
|
|
183
|
+
) -> Dict[str, Any]:
|
|
184
|
+
"""Send a message and get a response."""
|
|
185
|
+
try:
|
|
186
|
+
data = {
|
|
187
|
+
"conversation_id": conversation_id,
|
|
188
|
+
"model_name": model_name,
|
|
189
|
+
"user_message": user_message,
|
|
190
|
+
"image_urls": image_urls or [],
|
|
191
|
+
"audio_urls": audio_urls or [],
|
|
192
|
+
"video_urls": video_urls or [],
|
|
193
|
+
}
|
|
194
|
+
response = await self.async_client.post(
|
|
195
|
+
urljoin(self.server_url, "/send-message"),
|
|
196
|
+
json=data,
|
|
197
|
+
)
|
|
198
|
+
response.raise_for_status()
|
|
199
|
+
return response.json()
|
|
200
|
+
except httpx.HTTPError as exc:
|
|
201
|
+
logger.error("Error sending message: %s", exc)
|
|
202
|
+
raise RuntimeError(str(exc)) from exc
|
|
203
|
+
|
|
204
|
+
async def stream_message(
|
|
205
|
+
self,
|
|
206
|
+
conversation_id: str,
|
|
207
|
+
model_name: str,
|
|
208
|
+
user_message: str,
|
|
209
|
+
image_urls: Optional[List[str]] = None,
|
|
210
|
+
audio_urls: Optional[List[str]] = None,
|
|
211
|
+
video_urls: Optional[List[str]] = None,
|
|
212
|
+
) -> AsyncGenerator[Dict[str, Any], None]:
|
|
213
|
+
"""Stream a message and get responses."""
|
|
214
|
+
try:
|
|
215
|
+
data = {
|
|
216
|
+
"conversation_id": conversation_id,
|
|
217
|
+
"model_name": model_name,
|
|
218
|
+
"user_message": user_message,
|
|
219
|
+
"image_urls": image_urls or [],
|
|
220
|
+
"audio_urls": audio_urls or [],
|
|
221
|
+
"video_urls": video_urls or [],
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async with self.async_client.stream(
|
|
225
|
+
"POST",
|
|
226
|
+
urljoin(self.server_url, "/stream-message"),
|
|
227
|
+
json=data,
|
|
228
|
+
) as response:
|
|
229
|
+
response.raise_for_status()
|
|
230
|
+
|
|
231
|
+
async for line in response.aiter_lines():
|
|
232
|
+
if line.startswith("data: "):
|
|
233
|
+
try:
|
|
234
|
+
chunk = json.loads(line[6:])
|
|
235
|
+
if "error" in chunk:
|
|
236
|
+
raise RuntimeError(chunk["error"])
|
|
237
|
+
yield chunk
|
|
238
|
+
except json.JSONDecodeError as exc:
|
|
239
|
+
logger.error("Failed to parse stream chunk: %s", exc)
|
|
240
|
+
raise RuntimeError("Invalid stream response format") from exc
|
|
241
|
+
|
|
242
|
+
except httpx.HTTPError as exc:
|
|
243
|
+
logger.error("Stream error: %s", exc)
|
|
244
|
+
raise RuntimeError(str(exc)) from exc
|
|
245
|
+
|
|
246
|
+
async def generate_image(
|
|
247
|
+
self,
|
|
248
|
+
model_name: str,
|
|
249
|
+
prompt: str,
|
|
250
|
+
input_image_urls: Optional[List[str]] = None,
|
|
251
|
+
mask_url: Optional[str] = None,
|
|
252
|
+
generation_config: Optional[Dict[str, Any]] = None,
|
|
253
|
+
) -> Dict[str, Any]:
|
|
254
|
+
"""Generate or edit an image and return the server response."""
|
|
255
|
+
try:
|
|
256
|
+
data = {
|
|
257
|
+
"model_name": model_name,
|
|
258
|
+
"prompt": prompt,
|
|
259
|
+
"input_image_urls": input_image_urls or [],
|
|
260
|
+
"mask_url": mask_url,
|
|
261
|
+
"generation_config": generation_config or {},
|
|
262
|
+
}
|
|
263
|
+
response = await self.async_client.post(
|
|
264
|
+
urljoin(self.server_url, "/generate-image"),
|
|
265
|
+
json=data,
|
|
266
|
+
)
|
|
267
|
+
response.raise_for_status()
|
|
268
|
+
return response.json()
|
|
269
|
+
except httpx.HTTPError as exc:
|
|
270
|
+
logger.error("Error generating image: %s", exc)
|
|
271
|
+
raise RuntimeError(str(exc)) from exc
|
|
272
|
+
|
|
273
|
+
async def generate_speech(
|
|
274
|
+
self,
|
|
275
|
+
model_name: str,
|
|
276
|
+
prompt: str,
|
|
277
|
+
generation_config: Optional[Dict[str, Any]] = None,
|
|
278
|
+
) -> Dict[str, Any]:
|
|
279
|
+
"""Generate speech from text and return the server response."""
|
|
280
|
+
try:
|
|
281
|
+
data = {
|
|
282
|
+
"model_name": model_name,
|
|
283
|
+
"prompt": prompt,
|
|
284
|
+
"generation_config": generation_config or {},
|
|
285
|
+
}
|
|
286
|
+
response = await self.async_client.post(
|
|
287
|
+
urljoin(self.server_url, "/generate-speech"),
|
|
288
|
+
json=data,
|
|
289
|
+
)
|
|
290
|
+
response.raise_for_status()
|
|
291
|
+
return response.json()
|
|
292
|
+
except httpx.HTTPError as exc:
|
|
293
|
+
logger.error("Error generating speech: %s", exc)
|
|
294
|
+
raise RuntimeError(str(exc)) from exc
|
|
295
|
+
|
|
296
|
+
async def cleanup(self, conversation_id: str) -> Dict[str, Any]:
|
|
297
|
+
"""Clean up a conversation."""
|
|
298
|
+
try:
|
|
299
|
+
response = await self.async_client.post(
|
|
300
|
+
urljoin(self.server_url, "/cleanup"),
|
|
301
|
+
json={"conversation_id": conversation_id},
|
|
302
|
+
)
|
|
303
|
+
response.raise_for_status()
|
|
304
|
+
return response.json()
|
|
305
|
+
except httpx.HTTPError as exc:
|
|
306
|
+
logger.error("Cleanup error: %s", exc)
|
|
307
|
+
raise RuntimeError(str(exc)) from exc
|
|
308
|
+
|
|
309
|
+
async def close(self) -> None:
|
|
310
|
+
"""Close both HTTP clients."""
|
|
311
|
+
await self.async_client.aclose()
|
|
312
|
+
self.sync_client.close()
|
|
313
|
+
|
|
314
|
+
async def __aenter__(self) -> "AutobyteusClient":
|
|
315
|
+
return self
|
|
316
|
+
|
|
317
|
+
async def __aexit__(self, exc_type, exc, tb) -> None: # type: ignore[override]
|
|
318
|
+
await self.close()
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, Optional, Union
|
|
5
|
+
|
|
6
|
+
from cryptography import x509
|
|
7
|
+
from cryptography.hazmat.backends import default_backend
|
|
8
|
+
from cryptography.hazmat.primitives import hashes
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CertificateError(Exception):
|
|
14
|
+
"""Custom exception for certificate-related errors."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_certificate_info(cert_path: Union[str, Path]) -> Dict[str, object]:
|
|
18
|
+
"""
|
|
19
|
+
Retrieve certificate information including fingerprint and validity window.
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
cert_path = Path(cert_path)
|
|
23
|
+
cert_data = cert_path.read_bytes()
|
|
24
|
+
cert = x509.load_pem_x509_certificate(cert_data, default_backend())
|
|
25
|
+
|
|
26
|
+
cert_der = cert.fingerprint(hashes.SHA256())
|
|
27
|
+
fingerprint = ":".join(f"{byte:02X}" for byte in cert_der)
|
|
28
|
+
|
|
29
|
+
now = datetime.utcnow()
|
|
30
|
+
days_until_expiry = (cert.not_valid_after - now).days
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
"subject": cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[
|
|
34
|
+
0
|
|
35
|
+
].value,
|
|
36
|
+
"issuer": cert.issuer.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[
|
|
37
|
+
0
|
|
38
|
+
].value,
|
|
39
|
+
"valid_from": cert.not_valid_before,
|
|
40
|
+
"valid_until": cert.not_valid_after,
|
|
41
|
+
"fingerprint": fingerprint,
|
|
42
|
+
"is_valid": cert.not_valid_before < now < cert.not_valid_after,
|
|
43
|
+
"days_until_expiry": days_until_expiry,
|
|
44
|
+
"cert_data": cert_data,
|
|
45
|
+
"cert": cert,
|
|
46
|
+
}
|
|
47
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
48
|
+
raise CertificateError(f"Failed to get certificate info: {exc}") from exc
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def verify_certificate(
|
|
52
|
+
cert_path: Union[str, Path],
|
|
53
|
+
expected_fingerprint: Optional[str] = None,
|
|
54
|
+
warn_expiry_days: int = 30,
|
|
55
|
+
) -> Dict[str, object]:
|
|
56
|
+
"""
|
|
57
|
+
Verify the certificate's validity and optional fingerprint match.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
info = get_certificate_info(cert_path)
|
|
61
|
+
|
|
62
|
+
if not info["is_valid"]:
|
|
63
|
+
if datetime.utcnow() < info["valid_from"]:
|
|
64
|
+
raise CertificateError("Certificate is not yet valid")
|
|
65
|
+
raise CertificateError(
|
|
66
|
+
f"Certificate has expired on {info['valid_until'].strftime('%Y-%m-%d')}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if expected_fingerprint:
|
|
70
|
+
expected = expected_fingerprint.replace(" ", "").upper()
|
|
71
|
+
actual = str(info["fingerprint"]).replace(" ", "")
|
|
72
|
+
if actual != expected:
|
|
73
|
+
raise CertificateError(
|
|
74
|
+
"Certificate fingerprint mismatch. "
|
|
75
|
+
f"Expected: {expected}\nGot: {actual}"
|
|
76
|
+
)
|
|
77
|
+
logger.info("Certificate fingerprint verified successfully")
|
|
78
|
+
else:
|
|
79
|
+
logger.warning(
|
|
80
|
+
"Certificate fingerprint verification skipped. "
|
|
81
|
+
"Set AUTOBYTEUS_CERT_FINGERPRINT to enable this security feature. "
|
|
82
|
+
"Current certificate fingerprint: %s",
|
|
83
|
+
info["fingerprint"],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
logger.info(
|
|
87
|
+
"Certificate valid from %s to %s",
|
|
88
|
+
info["valid_from"],
|
|
89
|
+
info["valid_until"],
|
|
90
|
+
)
|
|
91
|
+
logger.info("Certificate fingerprint (SHA256): %s", info["fingerprint"])
|
|
92
|
+
logger.info("Certificate subject: %s", info["subject"])
|
|
93
|
+
|
|
94
|
+
if info["days_until_expiry"] <= warn_expiry_days:
|
|
95
|
+
logger.warning(
|
|
96
|
+
"Certificate will expire in %s days on %s",
|
|
97
|
+
info["days_until_expiry"],
|
|
98
|
+
info["valid_until"].strftime("%Y-%m-%d"),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return info
|
|
102
|
+
except CertificateError:
|
|
103
|
+
raise
|
|
104
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
105
|
+
raise CertificateError(f"Certificate verification failed: {exc}") from exc
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
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
|
|
33
|
+
m0StS7OaDEX18yPCiIz9DxVKlsPN
|
|
34
|
+
-----END CERTIFICATE-----
|
autobyteus/events/event_types.py
CHANGED
|
@@ -47,8 +47,8 @@ class EventType(Enum):
|
|
|
47
47
|
TEAM_STREAM_EVENT = "team_stream_event" # For unified agent team event stream
|
|
48
48
|
|
|
49
49
|
# --- Task Board Events ---
|
|
50
|
-
|
|
51
|
-
TASK_BOARD_STATUS_UPDATED = "
|
|
50
|
+
TASK_BOARD_TASKS_ADDED = "task_board.tasks.added"
|
|
51
|
+
TASK_BOARD_STATUS_UPDATED = "task_board.status.updated"
|
|
52
52
|
|
|
53
53
|
def __str__(self):
|
|
54
54
|
return self.value
|
|
@@ -5,7 +5,7 @@ from autobyteus.llm.utils.llm_config import LLMConfig
|
|
|
5
5
|
from autobyteus.llm.utils.token_usage import TokenUsage
|
|
6
6
|
from autobyteus.llm.utils.response_types import CompleteResponse, ChunkResponse
|
|
7
7
|
from autobyteus.llm.user_message import LLMUserMessage
|
|
8
|
-
from
|
|
8
|
+
from autobyteus.clients import AutobyteusClient
|
|
9
9
|
import logging
|
|
10
10
|
import uuid
|
|
11
11
|
|