161 lines
6.2 KiB
Python
161 lines
6.2 KiB
Python
from collections import defaultdict
|
|
from datetime import datetime, timezone
|
|
from enum import Enum, auto
|
|
from sys import exit
|
|
from threading import Lock
|
|
from typing import Callable, Dict, List, Optional
|
|
|
|
from .base_agent import BaseAgent
|
|
from .command import Command
|
|
from .command_result import CommandResult
|
|
from .iteration_logger import IterationLogger
|
|
from .llm_engine import LlmEngine
|
|
from .response_buffer import ResponseBuffer
|
|
from .response_parser import ResponseParser
|
|
from .system_metrics import SystemMetrics
|
|
from .working_memory import WorkingMemory
|
|
from .xml_validator import XMLValidator
|
|
|
|
class LlmState(Enum):
|
|
IDLE = auto()
|
|
INFERENCE = auto()
|
|
|
|
class WebAgent(BaseAgent):
|
|
def __init__(
|
|
self,
|
|
system_prompt: str,
|
|
action_schema: str,
|
|
working_memory: WorkingMemory,
|
|
metrics: SystemMetrics,
|
|
llms: Dict[str, LlmEngine],
|
|
validator: XMLValidator,
|
|
parser: ResponseParser,
|
|
iteration_logger: IterationLogger,
|
|
):
|
|
super().__init__(
|
|
system_prompt,
|
|
action_schema,
|
|
working_memory,
|
|
metrics,
|
|
validator,
|
|
parser
|
|
)
|
|
self._llms = llms
|
|
self._iteration_logger = iteration_logger
|
|
self._llm_states: Dict[str, LlmState] = {name: LlmState.IDLE for name in llms}
|
|
self._response_buffer: ResponseBuffer = ResponseBuffer()
|
|
self._validation_error: Optional[str] = None
|
|
self._command_result: Optional[CommandResult] = None
|
|
self._context = self._compile_context(next(iter(self._llms.values())))
|
|
self._stop_flags: Dict[str, bool] = {name: False for name in llms}
|
|
|
|
# Locks
|
|
self._llm_lock = Lock()
|
|
self._output_lock = Lock()
|
|
|
|
# Event handlers
|
|
self._llm_change_handlers: List[Callable[[str, LlmState], None]] = []
|
|
self._token_handlers: List[Callable[[str, str], None]] = []
|
|
self._context_change_handlers: List[Callable[[str, bool], None]] = []
|
|
|
|
# Change handlers
|
|
self._working_memory.add_change_handler(self._handle_memory_update)
|
|
|
|
@property
|
|
def llms(self) -> Dict[str, LlmState]:
|
|
"""Get current state of all LLMs"""
|
|
with self._llm_lock:
|
|
return self._llm_states.copy()
|
|
|
|
@property
|
|
def response_buffer(self) -> ResponseBuffer:
|
|
return self._response_buffer
|
|
|
|
@property
|
|
def context(self) -> str:
|
|
return self._context
|
|
|
|
@property
|
|
def command_result(self) -> Optional[CommandResult]:
|
|
return self._command_result
|
|
|
|
@property
|
|
def validation_error(self) -> Optional[str]:
|
|
return self._validation_error
|
|
|
|
def add_llm_change_handler(self, handler: Callable[[str, LlmState], None]) -> None:
|
|
"""Add handler for LLM state changes"""
|
|
if handler not in self._llm_change_handlers:
|
|
self._llm_change_handlers.append(handler)
|
|
|
|
def add_context_change_handler(self, handler: Callable[[str, bool], None]) -> None:
|
|
"""Add handler for context changes"""
|
|
if handler not in self._context_change_handlers:
|
|
self._context_change_handlers.append(handler)
|
|
|
|
def modify_context(self, context: str, generated: bool = False) -> None:
|
|
"""Update context and reset all LLM states"""
|
|
with self._llm_lock:
|
|
self._context = context
|
|
self._response_buffer.clear()
|
|
for llm_name in self._llms:
|
|
self._set_llm_state(llm_name, LlmState.IDLE)
|
|
|
|
for handler in self._context_change_handlers:
|
|
handler(context, generated)
|
|
|
|
def run_inference(self, llm_name: str) -> None:
|
|
"""Start inference on specified LLM"""
|
|
if llm_name not in self._llms:
|
|
raise ValueError(f"Unknown LLM: {llm_name}")
|
|
with self._llm_lock:
|
|
if self._llm_states[llm_name] != LlmState.IDLE:
|
|
raise RuntimeError(f"LLM {llm_name} is not ready for inference")
|
|
self._set_llm_state(llm_name, LlmState.INFERENCE)
|
|
self._stop_flags[llm_name] = False
|
|
llm = self._llms[llm_name]
|
|
def should_stop() -> bool:
|
|
return self._stop_flags[llm_name]
|
|
response_token_iter = llm.infer(self.system_prompt, self.context, self._response_buffer.get_text(), should_stop)
|
|
for token in response_token_iter:
|
|
print(token, end='', flush=True)
|
|
with self._output_lock:
|
|
self._response_buffer.append_text(token)
|
|
with self._llm_lock:
|
|
self._set_llm_state(llm_name, LlmState.IDLE)
|
|
|
|
def stop_inference(self, llm_name: str) -> None:
|
|
"""Stop ongoing inference for specified LLM"""
|
|
if llm_name not in self._llms:
|
|
raise ValueError(f"Unknown LLM: {llm_name}")
|
|
self._stop_flags[llm_name] = True
|
|
|
|
def approve_response(self) -> None:
|
|
"""Process approved response from specified LLM"""
|
|
if self.llms.get(llm_name) != LlmState.IDLE:
|
|
return
|
|
timestamp = datetime.now(timezone.utc)
|
|
self._iteration_logger.log_iteration(timestamp, self._context, self._response_buffer.get_text())
|
|
parse_result = self._parser.parse(timestamp, self._response_buffer.get_text())
|
|
if isinstance(parse_result, Command):
|
|
result = parse_result.execute(self._working_memory)
|
|
self._command_result = result
|
|
if result.should_stop:
|
|
exit(42)
|
|
else:
|
|
self._working_memory.update()
|
|
else:
|
|
parse_result.update()
|
|
self._working_memory.update()
|
|
self._working_memory.add_entry(parse_result)
|
|
|
|
def _set_llm_state(self, llm_name: str, state: LlmState) -> None:
|
|
"""Update LLM state and notify handlers"""
|
|
self._llm_states[llm_name] = state
|
|
for handler in self._llm_change_handlers:
|
|
handler(llm_name, state)
|
|
|
|
def _handle_memory_update(self) -> None:
|
|
"""Handle memory updates and update context"""
|
|
context = self._compile_context(next(iter(self._llms.values())))
|
|
self.modify_context(context, True) |