Approve context
This commit is contained in:
@@ -43,9 +43,20 @@ async def run_web_server():
|
||||
await server.start()
|
||||
|
||||
try:
|
||||
# Keep running until interrupted
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
if server._response_tasks:
|
||||
done, pending = await asyncio.wait(
|
||||
server._response_tasks,
|
||||
timeout=0.1,
|
||||
return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
for task in done:
|
||||
try:
|
||||
await task
|
||||
except Exception as e:
|
||||
print(f"Task error: {e}")
|
||||
server._response_tasks.remove(task)
|
||||
await asyncio.sleep(0.1)
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down...")
|
||||
finally:
|
||||
|
||||
@@ -54,7 +54,6 @@ class WebAgent(BaseAgent):
|
||||
self._command_result: Optional[CommandResult] = None
|
||||
self.context = self._compile_context()
|
||||
|
||||
|
||||
@property
|
||||
def current_state(self) -> WebAgentState:
|
||||
"""Get the current state of the agent."""
|
||||
@@ -93,43 +92,68 @@ class WebAgent(BaseAgent):
|
||||
def _notify_response_change(self) -> None:
|
||||
"""Notify all handlers of response change."""
|
||||
for handler in self._response_change_handlers:
|
||||
handler(self._response)
|
||||
handler(self.response)
|
||||
|
||||
def _set_response(self, response: str) -> None:
|
||||
"""Set response and notify handlers."""
|
||||
self.response = response
|
||||
self._validation_error = self._validator.validate(response)
|
||||
for handler in self._response_change_handlers:
|
||||
try:
|
||||
handler(response)
|
||||
except Exception as e:
|
||||
print(f"Error in response handler: {e}")
|
||||
|
||||
def _set_state(self, new_state: WebAgentState) -> None:
|
||||
"""
|
||||
Set the current response and notify handlers.
|
||||
Set the current state and notify handlers.
|
||||
|
||||
Args:
|
||||
response: New response
|
||||
new_state: New state to set
|
||||
"""
|
||||
self._response = response
|
||||
self._validation_error = self._validator.validate(self._response)
|
||||
self._notify_response_change()
|
||||
|
||||
def approve_context(self) -> None:
|
||||
if self._current_state != WebAgentState.CONTEXT_APPROVAL:
|
||||
raise Exception("Not in CONTEXT_APPROVAL state")
|
||||
|
||||
self._set_response("")
|
||||
self._current_state = WebAgentState.INFERENCE
|
||||
self._notify_state_change()
|
||||
|
||||
response_token_iter = self._llm.infer(self._system_prompt, self.context)
|
||||
|
||||
for token in response_token_iter:
|
||||
self._set_response(self._response + token)
|
||||
|
||||
self._current_state = WebAgentState.RESPONSE_APPROVAL
|
||||
self._current_state = new_state
|
||||
self._notify_state_change()
|
||||
|
||||
async def approve_context(self) -> None:
|
||||
if self._current_state != WebAgentState.CONTEXT_APPROVAL:
|
||||
error_msg = f"Not in CONTEXT_APPROVAL state (current: {self._current_state})"
|
||||
raise Exception(error_msg)
|
||||
|
||||
self._set_state(WebAgentState.INFERENCE)
|
||||
self._set_response("")
|
||||
|
||||
response_token_iter = self._llm.infer(self._system_prompt, self.context)
|
||||
response = ""
|
||||
try:
|
||||
for token in response_token_iter:
|
||||
response += token
|
||||
await self._set_response(response)
|
||||
print(f"{token}")
|
||||
except Exception as e:
|
||||
print(f"Error during inference: {e}")
|
||||
self._set_state(WebAgentState.CONTEXT_APPROVAL)
|
||||
return
|
||||
|
||||
await self._set_response(response)
|
||||
self._set_state(WebAgentState.RESPONSE_APPROVAL)
|
||||
|
||||
async def _set_response(self, response: str) -> None:
|
||||
"""Set response and notify handlers asynchronously."""
|
||||
self.response = response
|
||||
self._validation_error = self._validator.validate(response)
|
||||
for handler in self._response_change_handlers:
|
||||
try:
|
||||
await handler(response)
|
||||
except Exception as e:
|
||||
print(f"Error in response handler: {e}")
|
||||
|
||||
def approve_response(self) -> None:
|
||||
if self._current_state != WebAgentState.RESPONSE_APPROVAL:
|
||||
raise Exception("Not in RESPONSE_APPROVAL state")
|
||||
|
||||
self._current_state = WebAgentState.UPDATE
|
||||
self._notify_state_change()
|
||||
self._set_state(WebAgentState.UPDATE)
|
||||
|
||||
parse_result = self._parser.parse(self._response)
|
||||
parse_result = self._parser.parse(self.response)
|
||||
if isinstance(parse_result, Command):
|
||||
result = parse_result.execute(self._working_memory)
|
||||
self._command_result = result
|
||||
@@ -137,7 +161,6 @@ class WebAgent(BaseAgent):
|
||||
self._working_memory.add_entry(parse_result)
|
||||
|
||||
self._working_memory.update()
|
||||
self._context = self._compile_context()
|
||||
self.context = self._compile_context()
|
||||
|
||||
self._current_state = WebAgentState.CONTEXT_APPROVAL
|
||||
self._notify_state_change()
|
||||
self._set_state(WebAgentState.CONTEXT_APPROVAL)
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
from threading import Lock
|
||||
|
||||
from .io_buffer import IOBuffer
|
||||
|
||||
class WebIOBuffer(IOBuffer):
|
||||
"""
|
||||
IOBuffer implementation for web interface communication.
|
||||
|
||||
This class manages input/output through memory buffers instead of system IO,
|
||||
allowing for asynchronous communication through a web interface. It maintains
|
||||
separate buffers for stdin and stdout to simulate bidirectional communication.
|
||||
Thread-safe WebIOBuffer that maintains the synchronous IOBuffer interface.
|
||||
Uses threading primitives instead of asyncio for synchronization.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize empty input and output buffers."""
|
||||
self._stdin_buffer = ""
|
||||
self._stdout_buffer = ""
|
||||
self._lock = Lock()
|
||||
|
||||
def append_stdin(self, content: str) -> None:
|
||||
"""Thread-safe append to stdin buffer."""
|
||||
if not content:
|
||||
return
|
||||
|
||||
with self._lock:
|
||||
self._stdin_buffer += content
|
||||
|
||||
def read(self) -> str:
|
||||
"""
|
||||
Read and clear all available input from the stdin buffer.
|
||||
|
||||
Returns:
|
||||
str: Content from the stdin buffer, or empty string if buffer is empty
|
||||
"""
|
||||
content = self._stdin_buffer
|
||||
self._stdin_buffer = ""
|
||||
return content
|
||||
|
||||
"""Thread-safe read from stdin buffer."""
|
||||
with self._lock:
|
||||
content = self._stdin_buffer
|
||||
self._stdin_buffer = ""
|
||||
return content
|
||||
|
||||
def write(self, content: str) -> None:
|
||||
"""
|
||||
Append content to the stdout buffer.
|
||||
|
||||
Args:
|
||||
content: String content to append to the buffer
|
||||
"""
|
||||
if content:
|
||||
"""Thread-safe write to stdout buffer."""
|
||||
if not content:
|
||||
return
|
||||
|
||||
with self._lock:
|
||||
self._stdout_buffer += content
|
||||
|
||||
def buffer_length(self) -> int:
|
||||
@@ -55,14 +55,16 @@ class WebIOBuffer(IOBuffer):
|
||||
self._stdin_buffer += content
|
||||
|
||||
def get_stdout(self) -> str:
|
||||
"""
|
||||
Get the current content of the stdout buffer.
|
||||
|
||||
Returns:
|
||||
str: Current content of the stdout buffer
|
||||
"""
|
||||
return self._stdout_buffer
|
||||
|
||||
"""Thread-safe get stdout content."""
|
||||
with self._lock:
|
||||
return self._stdout_buffer
|
||||
|
||||
def clear_stdout(self) -> None:
|
||||
"""Clear the stdout buffer."""
|
||||
self._stdout_buffer = ""
|
||||
"""Thread-safe clear stdout buffer."""
|
||||
with self._lock:
|
||||
self._stdout_buffer = ""
|
||||
|
||||
def buffer_length(self) -> int:
|
||||
"""Thread-safe get stdin buffer length."""
|
||||
with self._lock:
|
||||
return len(self._stdin_buffer)
|
||||
|
||||
@@ -5,6 +5,7 @@ import asyncio
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
import mimetypes
|
||||
from functools import partial
|
||||
|
||||
from .web_agent import WebAgent, WebAgentState
|
||||
from .web_io_buffer import WebIOBuffer
|
||||
@@ -37,7 +38,7 @@ class WebServer:
|
||||
integrates with the WebAgent. Serves static files from a web directory.
|
||||
"""
|
||||
def __init__(self, agent: WebAgent, io_buffer: WebIOBuffer,
|
||||
host: str = "0.0.0.0", port: int = 8080,
|
||||
host: str = "0.0.0.0", port: int = 8080,
|
||||
static_dir: Optional[Path] = None):
|
||||
"""Initialize the web server."""
|
||||
self.agent = agent
|
||||
@@ -49,11 +50,22 @@ class WebServer:
|
||||
self._init_routes()
|
||||
self._clients: Set[web.WebSocketResponse] = set()
|
||||
self._lock = asyncio.Lock()
|
||||
self._response_tasks: List[asyncio.Task] = [] # Initialize tasks list
|
||||
self.runner: Optional[web.AppRunner] = None
|
||||
|
||||
# Register agent handlers
|
||||
self.agent.add_state_change_handler(self._handle_state_change)
|
||||
self.agent.add_response_change_handler(self._handle_response_change)
|
||||
# Register wrapped agent handlers
|
||||
self.agent.add_state_change_handler(self._wrap_async_handler(self._handle_state_change))
|
||||
self.agent.add_response_change_handler(self._wrap_async_handler(self._handle_response_change))
|
||||
|
||||
def _wrap_async_handler(self, coro_func):
|
||||
"""Wrap an async handler function to be called synchronously."""
|
||||
def wrapper(*args, **kwargs):
|
||||
loop = asyncio.get_event_loop()
|
||||
task = loop.create_task(coro_func(*args, **kwargs))
|
||||
self._response_tasks.append(task)
|
||||
self._response_tasks = [t for t in self._response_tasks if not t.done()]
|
||||
return task
|
||||
return wrapper
|
||||
|
||||
def _init_routes(self):
|
||||
"""Initialize application routes."""
|
||||
@@ -61,16 +73,44 @@ class WebServer:
|
||||
self.app.router.add_get("/ws", self._handle_websocket)
|
||||
|
||||
if self.static_dir.exists():
|
||||
self.app.router.add_get("/", self._serve_index)
|
||||
self.app.router.add_static("/static/", self.static_dir, show_index=False)
|
||||
self.app.router.add_static("/assets/", self.static_dir / "assets", show_index=False)
|
||||
self.app.router.add_get("/{path:.*}", self._serve_index)
|
||||
self.app.router.add_get("/", self._serve_index)
|
||||
self.app.router.add_static("/static/", self.static_dir, show_index=False)
|
||||
self.app.router.add_static("/assets/", self.static_dir / "assets", show_index=False)
|
||||
self.app.router.add_get("/{path:.*}", self._serve_index)
|
||||
|
||||
async def _handle_client_message(self, data: dict):
|
||||
"""Handle incoming client message."""
|
||||
message_type = data.get("type")
|
||||
|
||||
if message_type == ClientMessage.APPROVE_CONTEXT:
|
||||
if self.agent.current_state == WebAgentState.CONTEXT_APPROVAL:
|
||||
self.agent.approve_context()
|
||||
|
||||
elif message_type == ClientMessage.APPROVE_RESPONSE:
|
||||
if self.agent.current_state == WebAgentState.RESPONSE_APPROVAL:
|
||||
self.agent.approve_response()
|
||||
|
||||
elif message_type == ClientMessage.MODIFY_RESPONSE:
|
||||
if self.agent.current_state == WebAgentState.RESPONSE_APPROVAL:
|
||||
new_response = data.get("data")
|
||||
if new_response:
|
||||
self.agent._set_response(new_response)
|
||||
|
||||
elif message_type == ClientMessage.SEND_INPUT:
|
||||
input_text = data.get("data")
|
||||
if input_text:
|
||||
if not input_text.endswith('\n'):
|
||||
input_text += '\n'
|
||||
|
||||
self.io_buffer.append_stdin(input_text)
|
||||
|
||||
current_length = self.io_buffer.buffer_length()
|
||||
return
|
||||
else:
|
||||
print(f"Agent in wrong state: {self.agent.current_state}")
|
||||
|
||||
async def _serve_index(self, request: web.Request) -> web.Response:
|
||||
"""
|
||||
Serve the React application HTML for any unmatched routes to support
|
||||
client-side routing.
|
||||
"""
|
||||
"""Serve the React application HTML for any unmatched routes."""
|
||||
index_path = self.static_dir / "index.html"
|
||||
if not index_path.exists():
|
||||
raise web.HTTPNotFound()
|
||||
@@ -129,7 +169,8 @@ class WebServer:
|
||||
try:
|
||||
await ws.send_json(message)
|
||||
return True
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
print(f"Error sending to client: {e}")
|
||||
return False
|
||||
return False
|
||||
|
||||
@@ -216,27 +257,28 @@ class WebServer:
|
||||
return ws
|
||||
|
||||
async def _handle_client_message(self, data: dict):
|
||||
"""Handle incoming client message."""
|
||||
message_type = data.get("type")
|
||||
|
||||
|
||||
if message_type == ClientMessage.APPROVE_CONTEXT:
|
||||
if self.agent.current_state == WebAgentState.CONTEXT_APPROVAL:
|
||||
await self.agent.approve_context()
|
||||
|
||||
await self.agent.approve_context()
|
||||
elif message_type == ClientMessage.APPROVE_RESPONSE:
|
||||
if self.agent.current_state == WebAgentState.RESPONSE_APPROVAL:
|
||||
await self.agent.approve_response()
|
||||
self.agent.approve_response() # Now synchronous
|
||||
|
||||
elif message_type == ClientMessage.MODIFY_RESPONSE:
|
||||
if self.agent.current_state == WebAgentState.RESPONSE_APPROVAL:
|
||||
new_response = data.get("data")
|
||||
if new_response:
|
||||
await self.agent._set_response(new_response)
|
||||
self.agent._set_response(new_response)
|
||||
|
||||
elif message_type == ClientMessage.SEND_INPUT:
|
||||
input_text = data.get("data")
|
||||
if input_text:
|
||||
self.agent.set_input(input_text)
|
||||
self.io_buffer.append_stdin(input_text + "\n")
|
||||
else:
|
||||
print(f"Agent in wrong state: {self.agent.current_state}")
|
||||
|
||||
async def _handle_state_change(self, new_state: WebAgentState):
|
||||
"""Handle state changes from the WebAgent."""
|
||||
@@ -249,3 +291,27 @@ class WebServer:
|
||||
response,
|
||||
validation_error=self.agent._validation_error
|
||||
)
|
||||
|
||||
async def broadcast(self, message_type: str, data: str, **kwargs):
|
||||
"""Broadcast a message to all connected clients."""
|
||||
async with self._lock:
|
||||
message = {
|
||||
"type": message_type,
|
||||
"data": data,
|
||||
**kwargs
|
||||
}
|
||||
|
||||
tasks = []
|
||||
disconnected = set()
|
||||
|
||||
for ws in self._clients:
|
||||
if not ws.closed:
|
||||
try:
|
||||
await self._send_to_client(ws, message)
|
||||
except Exception as e:
|
||||
print(f"Error sending to client: {e}")
|
||||
disconnected.add(ws)
|
||||
else:
|
||||
disconnected.add(ws)
|
||||
|
||||
self._clients -= disconnected
|
||||
Reference in New Issue
Block a user