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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +6 -2
  2. autobyteus/agent/handlers/inter_agent_message_event_handler.py +17 -19
  3. autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +6 -3
  4. autobyteus/agent/handlers/tool_result_event_handler.py +61 -18
  5. autobyteus/agent/handlers/user_input_message_event_handler.py +19 -10
  6. autobyteus/agent/hooks/base_phase_hook.py +17 -0
  7. autobyteus/agent/hooks/hook_registry.py +15 -27
  8. autobyteus/agent/input_processor/base_user_input_processor.py +17 -1
  9. autobyteus/agent/input_processor/processor_registry.py +15 -27
  10. autobyteus/agent/llm_response_processor/base_processor.py +17 -1
  11. autobyteus/agent/llm_response_processor/processor_registry.py +15 -24
  12. autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +14 -0
  13. autobyteus/agent/message/agent_input_user_message.py +15 -2
  14. autobyteus/agent/message/send_message_to.py +1 -1
  15. autobyteus/agent/processor_option.py +17 -0
  16. autobyteus/agent/sender_type.py +1 -0
  17. autobyteus/agent/system_prompt_processor/base_processor.py +17 -1
  18. autobyteus/agent/system_prompt_processor/processor_registry.py +15 -27
  19. autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +10 -0
  20. autobyteus/agent/tool_execution_result_processor/base_processor.py +17 -1
  21. autobyteus/agent/tool_execution_result_processor/processor_registry.py +15 -1
  22. autobyteus/agent/workspace/base_workspace.py +1 -1
  23. autobyteus/agent/workspace/workspace_definition.py +1 -1
  24. autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +1 -1
  25. autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +2 -2
  26. autobyteus/agent_team/task_notification/__init__.py +4 -0
  27. autobyteus/agent_team/task_notification/activation_policy.py +70 -0
  28. autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +56 -122
  29. autobyteus/agent_team/task_notification/task_activator.py +66 -0
  30. autobyteus/cli/agent_team_tui/state.py +17 -20
  31. autobyteus/cli/agent_team_tui/widgets/focus_pane.py +1 -1
  32. autobyteus/cli/agent_team_tui/widgets/task_board_panel.py +1 -1
  33. autobyteus/clients/__init__.py +10 -0
  34. autobyteus/clients/autobyteus_client.py +318 -0
  35. autobyteus/clients/cert_utils.py +105 -0
  36. autobyteus/clients/certificates/cert.pem +34 -0
  37. autobyteus/events/event_types.py +2 -2
  38. autobyteus/llm/api/autobyteus_llm.py +1 -1
  39. autobyteus/llm/api/gemini_llm.py +45 -54
  40. autobyteus/llm/api/qwen_llm.py +25 -0
  41. autobyteus/llm/api/zhipu_llm.py +26 -0
  42. autobyteus/llm/autobyteus_provider.py +9 -3
  43. autobyteus/llm/llm_factory.py +39 -0
  44. autobyteus/llm/ollama_provider_resolver.py +1 -0
  45. autobyteus/llm/providers.py +1 -0
  46. autobyteus/llm/token_counter/token_counter_factory.py +3 -0
  47. autobyteus/llm/token_counter/zhipu_token_counter.py +24 -0
  48. autobyteus/multimedia/audio/api/autobyteus_audio_client.py +5 -2
  49. autobyteus/multimedia/audio/api/gemini_audio_client.py +84 -153
  50. autobyteus/multimedia/audio/audio_client_factory.py +47 -22
  51. autobyteus/multimedia/audio/audio_model.py +13 -6
  52. autobyteus/multimedia/audio/autobyteus_audio_provider.py +9 -3
  53. autobyteus/multimedia/audio/base_audio_client.py +3 -1
  54. autobyteus/multimedia/image/api/autobyteus_image_client.py +13 -6
  55. autobyteus/multimedia/image/api/gemini_image_client.py +72 -130
  56. autobyteus/multimedia/image/api/openai_image_client.py +4 -2
  57. autobyteus/multimedia/image/autobyteus_image_provider.py +9 -3
  58. autobyteus/multimedia/image/base_image_client.py +6 -2
  59. autobyteus/multimedia/image/image_client_factory.py +20 -19
  60. autobyteus/multimedia/image/image_model.py +13 -6
  61. autobyteus/multimedia/providers.py +1 -0
  62. autobyteus/task_management/__init__.py +10 -10
  63. autobyteus/task_management/base_task_board.py +14 -6
  64. autobyteus/task_management/converters/__init__.py +0 -2
  65. autobyteus/task_management/converters/task_board_converter.py +7 -16
  66. autobyteus/task_management/events.py +6 -6
  67. autobyteus/task_management/in_memory_task_board.py +48 -38
  68. autobyteus/task_management/schemas/__init__.py +2 -2
  69. autobyteus/task_management/schemas/{plan_definition.py → task_definition.py} +6 -7
  70. autobyteus/task_management/schemas/task_status_report.py +1 -2
  71. autobyteus/task_management/task.py +60 -0
  72. autobyteus/task_management/tools/__init__.py +6 -2
  73. autobyteus/task_management/tools/assign_task_to.py +125 -0
  74. autobyteus/task_management/tools/get_my_tasks.py +80 -0
  75. autobyteus/task_management/tools/get_task_board_status.py +3 -3
  76. autobyteus/task_management/tools/publish_task.py +77 -0
  77. autobyteus/task_management/tools/publish_tasks.py +74 -0
  78. autobyteus/task_management/tools/update_task_status.py +5 -5
  79. autobyteus/tools/__init__.py +54 -16
  80. autobyteus/tools/base_tool.py +4 -4
  81. autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +1 -1
  82. autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +1 -1
  83. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +1 -1
  84. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +1 -1
  85. autobyteus/tools/browser/standalone/navigate_to.py +1 -1
  86. autobyteus/tools/browser/standalone/web_page_pdf_generator.py +1 -1
  87. autobyteus/tools/browser/standalone/webpage_image_downloader.py +1 -1
  88. autobyteus/tools/browser/standalone/webpage_reader.py +1 -1
  89. autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +1 -1
  90. autobyteus/tools/download_media_tool.py +136 -0
  91. autobyteus/tools/file/file_editor.py +200 -0
  92. autobyteus/tools/functional_tool.py +1 -1
  93. autobyteus/tools/google_search.py +1 -1
  94. autobyteus/tools/mcp/factory.py +1 -1
  95. autobyteus/tools/mcp/schema_mapper.py +1 -1
  96. autobyteus/tools/mcp/tool.py +1 -1
  97. autobyteus/tools/multimedia/__init__.py +2 -0
  98. autobyteus/tools/multimedia/audio_tools.py +10 -20
  99. autobyteus/tools/multimedia/image_tools.py +21 -22
  100. autobyteus/tools/multimedia/media_reader_tool.py +117 -0
  101. autobyteus/tools/pydantic_schema_converter.py +1 -1
  102. autobyteus/tools/registry/tool_definition.py +1 -1
  103. autobyteus/tools/timer.py +1 -1
  104. autobyteus/tools/tool_meta.py +1 -1
  105. autobyteus/tools/usage/formatters/default_json_example_formatter.py +1 -1
  106. autobyteus/tools/usage/formatters/default_xml_example_formatter.py +1 -1
  107. autobyteus/tools/usage/formatters/default_xml_schema_formatter.py +59 -3
  108. autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +1 -1
  109. autobyteus/tools/usage/formatters/google_json_example_formatter.py +1 -1
  110. autobyteus/tools/usage/formatters/openai_json_example_formatter.py +1 -1
  111. autobyteus/tools/usage/parsers/_string_decoders.py +18 -0
  112. autobyteus/tools/usage/parsers/default_json_tool_usage_parser.py +9 -1
  113. autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +15 -1
  114. autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +4 -1
  115. autobyteus/tools/usage/parsers/openai_json_tool_usage_parser.py +4 -1
  116. autobyteus/{tools → utils}/parameter_schema.py +1 -1
  117. {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/METADATA +4 -3
  118. {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/RECORD +122 -108
  119. examples/run_poem_writer.py +1 -1
  120. autobyteus/task_management/converters/task_plan_converter.py +0 -48
  121. autobyteus/task_management/task_plan.py +0 -110
  122. autobyteus/task_management/tools/publish_task_plan.py +0 -101
  123. autobyteus/tools/image_downloader.py +0 -99
  124. autobyteus/tools/pdf_downloader.py +0 -89
  125. {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/WHEEL +0 -0
  126. {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/licenses/LICENSE +0 -0
  127. {autobyteus-1.1.8.dist-info → autobyteus-1.2.0.dist-info}/top_level.txt +0 -0
@@ -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.task_plan import Task
20
- from autobyteus.task_management.events import TaskPlanPublishedEvent, TaskStatusUpdatedEvent
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, TaskPlanPublishedEvent):
93
- self._task_plans[team_name_key] = event.data.plan.tasks
94
- # Reset statuses when a new plan is published
95
- self._task_statuses[team_name_key] = {task.task_id: TaskStatus.NOT_STARTED for task in event.data.plan.tasks}
96
- logger.debug(f"TUI State: Updated task plan for '{team_name_key}' with {len(event.data.plan.tasks)} tasks.")
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
- # Update status
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
- # Update deliverables if they are provided in the event.
105
- if event.data.deliverables is not None:
106
- if team_name_key in self._task_plans:
107
- for task in self._task_plans[team_name_key]:
108
- if task.task_id == event.data.task_id:
109
- task.file_deliverables = event.data.deliverables
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.task_plan import Task
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.task_plan import Task
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-----
@@ -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
- TASK_BOARD_PLAN_PUBLISHED = "task_board_plan_published"
51
- TASK_BOARD_STATUS_UPDATED = "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 autobyteus_llm_client.client import AutobyteusClient
8
+ from autobyteus.clients import AutobyteusClient
9
9
  import logging
10
10
  import uuid
11
11