Add auto_approver
This commit is contained in:
@@ -122,5 +122,5 @@ if __name__ == "__main__":
|
||||
loop = asyncio.new_event_loop()
|
||||
config = Config()
|
||||
main = loop.run_until_complete(Main.create(config))
|
||||
print(f"Web server started at http://{config.host}:{config.port}")
|
||||
print(f"Web server started at http://localhost:{config.port}")
|
||||
web.run_app(main.app, loop=loop, host=config.host, port=config.port)
|
||||
162
sia/auto_approver.py
Normal file
162
sia/auto_approver.py
Normal file
@@ -0,0 +1,162 @@
|
||||
from threading import Thread, Event
|
||||
from typing import Callable, TypeAlias, TypedDict
|
||||
from .web_agent import WebAgent, WebAgentState
|
||||
|
||||
class AutoApproverConfig(TypedDict):
|
||||
context_enabled: bool
|
||||
response_enabled: bool
|
||||
context_timeout: float
|
||||
response_timeout: float
|
||||
|
||||
ConfigChangeHandler: TypeAlias = Callable[[AutoApproverConfig], None]
|
||||
|
||||
class AutoApprover:
|
||||
"""
|
||||
Handles automatic approvals for WebAgent states after configurable timeouts.
|
||||
"""
|
||||
|
||||
def __init__(self, agent: WebAgent):
|
||||
"""
|
||||
Initialize auto approver with a WebAgent instance.
|
||||
"""
|
||||
self.agent = agent
|
||||
self._context_timeout = 5.0
|
||||
self._response_timeout = 10.0
|
||||
self._context_enabled = False
|
||||
self._response_enabled = False
|
||||
self._stop_event = Event()
|
||||
self._context_thread: Thread | None = None
|
||||
self._response_thread: Thread | None = None
|
||||
self._config_change_handlers: list[ConfigChangeHandler] = []
|
||||
|
||||
self.agent.add_state_change_handler(self._handle_state_change)
|
||||
|
||||
@property
|
||||
def config(self) -> AutoApproverConfig:
|
||||
return AutoApproverConfig(
|
||||
context_enabled=self._context_enabled,
|
||||
response_enabled=self._response_enabled,
|
||||
context_timeout=self._context_timeout,
|
||||
response_timeout=self._response_timeout
|
||||
)
|
||||
|
||||
def set_config(self, config: AutoApproverConfig) -> None:
|
||||
self._context_enabled = config['context_enabled']
|
||||
self._response_enabled = config['response_enabled']
|
||||
self._context_timeout = config['context_timeout']
|
||||
self._response_timeout = config['response_timeout']
|
||||
|
||||
if self._context_enabled and self.agent.state == WebAgentState.CONTEXT_APPROVAL:
|
||||
self._start_context_thread()
|
||||
else:
|
||||
self._stop_context_thread()
|
||||
|
||||
if self._response_enabled and self.agent.state == WebAgentState.RESPONSE_APPROVAL:
|
||||
self._start_response_thread()
|
||||
else:
|
||||
self._stop_response_thread()
|
||||
|
||||
self._notify_config_change()
|
||||
|
||||
def add_config_change_handler(self, handler: ConfigChangeHandler) -> None:
|
||||
self._config_change_handlers.append(handler)
|
||||
|
||||
def _notify_config_change(self) -> None:
|
||||
current_config = self.config
|
||||
for handler in self._config_change_handlers:
|
||||
handler(current_config)
|
||||
|
||||
@property
|
||||
def context_timeout(self) -> float:
|
||||
return self._context_timeout
|
||||
|
||||
@context_timeout.setter
|
||||
def context_timeout(self, timeout: float) -> None:
|
||||
if self._context_enabled:
|
||||
raise ValueError("Cannot change timeout while auto-approval is enabled")
|
||||
self._context_timeout = timeout
|
||||
self._notify_config_change()
|
||||
|
||||
@property
|
||||
def response_timeout(self) -> float:
|
||||
return self._response_timeout
|
||||
|
||||
@response_timeout.setter
|
||||
def response_timeout(self, timeout: float) -> None:
|
||||
if self._response_enabled:
|
||||
raise ValueError("Cannot change timeout while auto-approval is enabled")
|
||||
self._response_timeout = timeout
|
||||
self._notify_config_change()
|
||||
|
||||
@property
|
||||
def context_enabled(self) -> bool:
|
||||
return self._context_enabled
|
||||
|
||||
@context_enabled.setter
|
||||
def context_enabled(self, enabled: bool) -> None:
|
||||
if enabled == self._context_enabled:
|
||||
return
|
||||
self._context_enabled = enabled
|
||||
self._stop_context_thread()
|
||||
if enabled and self.agent.state == WebAgentState.CONTEXT_APPROVAL:
|
||||
self._start_context_thread()
|
||||
self._notify_config_change()
|
||||
|
||||
@property
|
||||
def response_enabled(self) -> bool:
|
||||
return self._response_enabled
|
||||
|
||||
@response_enabled.setter
|
||||
def response_enabled(self, enabled: bool) -> None:
|
||||
if enabled == self._response_enabled:
|
||||
return
|
||||
self._response_enabled = enabled
|
||||
self._stop_response_thread()
|
||||
if enabled and self.agent.state == WebAgentState.RESPONSE_APPROVAL:
|
||||
self._start_response_thread()
|
||||
self._notify_config_change()
|
||||
|
||||
def _handle_state_change(self, new_state: WebAgentState) -> None:
|
||||
if new_state == WebAgentState.CONTEXT_APPROVAL:
|
||||
self._start_context_thread()
|
||||
elif self._context_enabled:
|
||||
self._stop_context_thread()
|
||||
|
||||
if new_state == WebAgentState.RESPONSE_APPROVAL:
|
||||
self._start_response_thread()
|
||||
elif self._response_enabled:
|
||||
self._stop_response_thread()
|
||||
|
||||
def _stop_context_thread(self) -> None:
|
||||
if self._context_thread:
|
||||
self._stop_event.set()
|
||||
self._context_thread = None
|
||||
self._stop_event.clear()
|
||||
|
||||
def _stop_response_thread(self) -> None:
|
||||
if self._response_thread:
|
||||
self._stop_event.set()
|
||||
self._response_thread = None
|
||||
self._stop_event.clear()
|
||||
|
||||
def _start_context_thread(self) -> None:
|
||||
self._context_thread = Thread(target=self._context_approval_thread)
|
||||
self._context_thread.start()
|
||||
|
||||
def _start_response_thread(self) -> None:
|
||||
self._response_thread = Thread(target=self._response_approval_thread)
|
||||
self._response_thread.start()
|
||||
|
||||
def _context_approval_thread(self) -> None:
|
||||
if self._stop_event.wait(self._context_timeout):
|
||||
return
|
||||
if (self._context_enabled and
|
||||
self.agent.state == WebAgentState.CONTEXT_APPROVAL):
|
||||
self.agent.approve_context()
|
||||
|
||||
def _response_approval_thread(self) -> None:
|
||||
if self._stop_event.wait(self._response_timeout):
|
||||
return
|
||||
if (self._response_enabled and
|
||||
self.agent.state == WebAgentState.RESPONSE_APPROVAL):
|
||||
self.agent.approve_response()
|
||||
@@ -6,6 +6,7 @@ from dataclasses import dataclass
|
||||
|
||||
from .web_agent import WebAgent, WebAgentState
|
||||
from .web_io_buffer import WebIOBuffer
|
||||
from .auto_approver import AutoApprover, AutoApproverConfig
|
||||
|
||||
@dataclass
|
||||
class ClientMessage:
|
||||
@@ -15,6 +16,7 @@ class ClientMessage:
|
||||
MODIFY_RESPONSE = "MODIFY_RESPONSE"
|
||||
SEND_INPUT = "SEND_INPUT"
|
||||
CLEAR_OUTPUT = "CLEAR_OUTPUT"
|
||||
AUTO_APPROVER_CONFIG = "AUTO_APPROVER_CONFIG"
|
||||
|
||||
@dataclass
|
||||
class ServerMessage:
|
||||
@@ -23,6 +25,7 @@ class ServerMessage:
|
||||
CONTEXT_UPDATE = "CONTEXT_UPDATE"
|
||||
RESPONSE_UPDATE = "RESPONSE_UPDATE"
|
||||
OUTPUT_UPDATE = "OUTPUT_UPDATE"
|
||||
AUTO_APPROVER_CONFIG = "AUTO_APPROVER_CONFIG"
|
||||
|
||||
class WebSocketManager:
|
||||
"""
|
||||
@@ -38,11 +41,13 @@ class WebSocketManager:
|
||||
self.agent = agent
|
||||
self.io_buffer = io_buffer
|
||||
self._clients: Set[web.WebSocketResponse] = set()
|
||||
self.auto_approver = AutoApprover(agent)
|
||||
|
||||
self.agent.add_state_change_handler(self._wrap_async(self._handle_state_change))
|
||||
self.agent.add_context_change_handler(self._wrap_async(self._handle_context_change))
|
||||
self.agent.add_response_change_handler(self._wrap_async(self._handle_response_change))
|
||||
self.io_buffer.add_stdout_change_handler(self._wrap_async(self._handle_stdout_change))
|
||||
self.auto_approver.add_config_change_handler(self._wrap_async(self._handle_auto_approver_config))
|
||||
return self
|
||||
|
||||
async def _broadcast_message(self, message: Dict):
|
||||
@@ -99,6 +104,13 @@ class WebSocketManager:
|
||||
"output": output
|
||||
})
|
||||
|
||||
async def _handle_auto_approver_config(self, config: AutoApproverConfig):
|
||||
"""Handle auto approver config changes."""
|
||||
await self._broadcast_message({
|
||||
"type": ServerMessage.AUTO_APPROVER_CONFIG,
|
||||
"config": config
|
||||
})
|
||||
|
||||
async def _handle_client_message(self, request: web.Request, data: dict[str, Any]):
|
||||
"""Handle incoming client message."""
|
||||
message_type = data.get("type")
|
||||
@@ -117,9 +129,13 @@ class WebSocketManager:
|
||||
if self.agent.state == WebAgentState.RESPONSE_APPROVAL:
|
||||
self.agent.modify_response(data.get("response"))
|
||||
elif message_type == ClientMessage.SEND_INPUT:
|
||||
self.io_buffer.append_stdin(data.get("input"))
|
||||
self.io_buffer.append_stdin(data.get("input"))
|
||||
elif message_type == ClientMessage.CLEAR_OUTPUT:
|
||||
self.io_buffer.clear_stdout()
|
||||
elif message_type == ClientMessage.AUTO_APPROVER_CONFIG:
|
||||
config = data.get("config")
|
||||
if isinstance(config, dict):
|
||||
self.auto_approver.set_config(config)
|
||||
|
||||
async def handle_websocket(self, request: web.Request) -> web.WebSocketResponse:
|
||||
"""Handle new WebSocket connections and messages."""
|
||||
@@ -142,6 +158,10 @@ class WebSocketManager:
|
||||
"response": self.agent.response,
|
||||
"validation_error": self.agent._validation_error
|
||||
})
|
||||
await ws.send_json({
|
||||
"type": ServerMessage.AUTO_APPROVER_CONFIG,
|
||||
"config": self.auto_approver.config
|
||||
})
|
||||
async for msg in ws:
|
||||
if msg.type == WSMsgType.TEXT:
|
||||
data = json.loads(msg.data)
|
||||
@@ -150,4 +170,4 @@ class WebSocketManager:
|
||||
print(f"WebSocket connection closed with error: {ws.exception()}")
|
||||
finally:
|
||||
self._clients.remove(ws)
|
||||
return ws
|
||||
return ws
|
||||
|
||||
Reference in New Issue
Block a user