Add auto_approver

This commit is contained in:
Niels Geens
2024-11-15 17:09:07 +01:00
parent 4ce421bbce
commit 9dfd5ddc39
9 changed files with 449 additions and 25 deletions

View File

@@ -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
View 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()

View File

@@ -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