Update readme for separate llm engine executables

This commit is contained in:
Niels Geens
2025-05-10 17:09:29 +02:00
parent f8365ef698
commit 895a533e01
5 changed files with 297 additions and 233 deletions

275
README.md
View File

@@ -112,89 +112,258 @@ There are only a few core actions:
- Writing to standard output
- Reasoning
### Scripts
#### Scripts
Scripts can run in one of 2 modes: single-shot or repeat.
Their mode and output (stdout and stderr) stay in the context until they are explicitly removed.
In this way the agent manages what information is available in the context.
#### Single-shot script
##### Single-shot script
The script is executed once.
This is useful for most operations e.g. writing to or moving a file or downloading content from the internet.
The next iteration starts after the scripts has finished.
#### Repeat script
##### Repeat script
The script is restarted on each iteration.
This is useful for monitoring files or the file system.
commands like `head` and `tail` can be used to limit the data in context.
The next iteration starts after all repeat scripts in context have finished.
### Use of XML
### Processes in SIA
The context and actions are formatted as XML.
For the context this adds clear rules for escaping.
This is usefull in case a previous context is embedded.
SIA operates through a coordinated system of processes, each with specialized responsibilities.
This choice is driven by dependency isolation for the llm engine implementations,
and the ability to use namespaces for process isolation of sia instances.
The LLM is free to escape data any way it wants,
as long as it results in valid XML.
The response is validated against a schema.
#### Main SIA Process
#### XML Data Flow
Entries store their content as raw text. During context compilation, the XML formatter
wraps text content in CDATA sections, except when the content contains CDATA closing sequences.
In those cases, the formatter uses standard XML escaping.
The core SIA application runs as a continuous process that cycles through its context generation, LLM interaction, and action execution loop.
This process is typically managed by the `restart.sh` script, which ensures SIA restarts whenever it stops.
This restart mechanism is a critical part of how SIA implements self-improvement:
This separation between storage and formatting:
- Keeps entry data clean and unescaped
- Centralizes XML formatting rules
- Makes it easy to change escaping rules without modifying entries
- Allows different formatting for different use cases
1. When SIA makes changes to its own code, it terminates with a special exit code (42)
2. The restart script detects this exit code and restarts SIA
3. Upon restart, SIA loads the modified Python files, effectively "installing" its own updates
The Context is escaped using CDATA blocks.
Except when the data contains CDATA closing sequences.
Then the whole block is escaped using standard XML escaping.
#### Testing Instances
SIA can create isolated test instances of itself to evaluate improvements and test capabilities.
These instances run in separate process spaces with their own resources and filesystem views, managed by the tool Bubblewrap.
This isolation ensures that test instances don't interfere with each other or the main SIA instance while allowing observation of their behavior.
Sub instances are explained in `procedures/self_improvement/reasoning.md`.
### The SIA process
SIA is typically runs with the `restart.sh` script.
This is a simple shell script that runs SIA in a loop.
When stopped, SIA restarts and reloads the Python files.
This is how SIA can self-update.
SIA can also run SIA processes as script.
This can be used for testing updates to the LLM or core functionality.
### Server for debuggin and human input
#### Web Server for Human Interaction
SIA can be started with an optional `--server` flag.
This starts a web server that can be used to interact with SIA.
It is made, specifically for reinforcement learning by human feedback.
It is made for debugging and stearing the model until it is properly trained.
The web interface takes over standard input and output.
It will display the context for editing before handing it to the LLM.
After each run of the LLM, before parsing, it will display the reasoning and actions.
It interactively displays if the actions can be parsed.
At any time, the user can write to the standard input of SIA.
After each run of the LLM, before parsing, it will display the response.
## Architecture
The web server uses WebSockets to maintain real-time communication with connected clients, broadcasting state updates as they occur and processing commands from the interface.
SIA follows a modular architecture centered around an agent that processes context through an LLM to generate actions.
The system can run in two modes: a standard command-line mode and an interactive web mode for debugging and human feedback.
#### LLM Engine Subprocesses
SIA communicates with LLM engines through dedicated subprocesses rather than directly integrating them into the main application.
Each LLM type (Gemma, QwQ, Mistral, etc.) runs in its own subprocess with a tailored environment.
This architecture provides several advantages:
1. **Dependency Isolation**: Different LLM implementations often have conflicting dependency requirements.
By running each in a separate subprocess with its own virtual environment, these conflicts are avoided.
2. **Resource Management**: LLM engines can be resource-intensive.
The subprocess approach allows for clean termination and resource reclamation when switching between models or canceling generation.
3. **Implementation Simplicity**: New LLM types can be added by implementing a focused subprocess runner without modifying the core SIA agent code.
### Use of XML
XML plays several crucial roles throughout SIA's architecture as a structured data format for different communication interfaces.
The consistent use of XML throughout SIA provides a unified approach to data representation, validation, and communication between components.
But because of how SIA operates it's necessary to treat some data as plain text.
#### Context and Entry Representation
The context and entries are formatted as XML before presenting them to the LLM.
CDATA sections keep escaping to a minimum.
This would not be the case when using e.g. json.
Entry id's and the delete action allow the LLM to manage it's own context.
**Example:**
```xml
<context
time="2024-10-18T12:00:00Z"
memory_used="9556302234"
memory_total="17179869184">
<repeat id="a3d89ee5-28ec-4c5a-b9e9-a30af53d43a0" exit_code="0">
<![CDATA[ls -lah /root/data]]>
<stdout>
<![CDATA[total 16K
drwxr-xr-x 1 sia 1049089 0 Oct 28 13:40 ./
drwxr-xr-x 1 sia 1049089 0 Oct 28 13:40 ../
]]>
</stdout>
<stderr/>
</repeat>
</context>
```
#### XML formatting
During context compilation, the XML formatter wraps text content in CDATA sections when possible,
falling back to standard XML escaping when content contains CDATA closing sequences (`]]>`).
Because of how newlines are added for formatting, all data should be trimmed from leading and trailing whitespace when reading.
**Example of CDATA usage:**
```xml
<single id="12345678">
<![CDATA[echo "Hello world" > /tmp/test.txt]]>
<stdout>
<![CDATA[]]>
</stdout>
<stderr/>
</single>
```
**Example of XML escaping when CDATA cannot be used:**
```xml
<reasoning id="87654321">
I noticed that the file contains a CDATA end marker like this: ]]&gt;
I need to be careful when processing this content.
</reasoning>
```
Notice how the content of this rendered entry can differ from the generated text.
The LLM needs to be trained to handle this properly.
#### XML Schema Validation
Responses from the LLM are validated against an XML schema (`action_schema.xsd`) that defines the structure and requirements for valid actions.
This ensures only supported actions are executed with required attributes.
**Example schema definition:**
```xml
<xs:element name="single">
<xs:complexType mixed="true">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
</xs:sequence>
<xs:attribute name="timeout" type="xs:float" use="optional"/>
<xs:attribute name="limit" type="xs:integer" use="optional"/>
</xs:complexType>
</xs:element>
```
**Example validation error:**
```xml
<parse_error id="20240512_123456_789">
<error>Missing required attribute 'id' on element 'delete'</error>
<content>
<![CDATA[<delete/>]]>
</content>
</parse_error>
```
#### Logits Processing with XML Schema
SIA uses a custom XML schema validator (`lib/xml_schema_validator`) that can operate on token probabilities during text generation.
This guides the model toward valid XML structures.
It is most helpful when creating training data.
Logits processing is computationally expensive.
It is not supported for all LLM implementations so the SIA core should not make assumptions on the generated text.
#### Iteration Logging
All iterations of context-response pairs are stored in XML files, providing a structured record of agent behavior.
**Example iteration log file:**
```xml
<iteration system_prompt_hash="a1b2c3d4" action_schema_hash="e5f6g7h8">
<context>
<context time="2024-10-18T12:00:00Z" memory_used="9556302234" memory_total="17179869184">
<repeat id="a3d89ee5-28ec-4c5a-b9e9-a30af53d43a0" exit_code="0">
<![CDATA[ls -lah /root/data]]>
<stdout><![CDATA[total 16K
drwxr-xr-x 1 sia 1049089 0 Oct 28 13:40 ./
]]></stdout>
<stderr/>
</repeat>
</context>
</context>
<response>&lt;reasoning&gt;
I should check what files are in the tasks directory to see if there are any pending tasks.
&lt;/reasoning&gt;</response>
</iteration>
```
Notice how the response is stored as plaintext, even though it contains an xml reasoning action.
When saving the iteration, the response is not parsed or validated yet.
Storing the response as plaintext helps debugging and retains info that would otherwise be lost.
E.g. delete actions do not create an entry and would be harder to find.
Or xml comments used for inline reasoning are not saved after parsing.
#### LLM Engine Communication
LLM engine subprocesses receive input as XML documents containing paths to required files and the context.
**Example input to LLM engine subprocess:**
```xml
<input>
<system_prompt_path>/root/sia/system_prompt.md</system_prompt_path>
<action_schema_path>/root/sia/action_schema.xsd</action_schema_path>
<context>
<context time="2024-10-18T12:00:00Z" memory_used="9556302234" memory_total="17179869184">
<!-- Working memory entries -->
</context>
</context>
<prefix><!-- Optional existing text to continue --></prefix>
</input>
```
Though the LLM can output any text, the goal is to output valid xml.
The logits processor can help enfoce this but ultimately the core application is responsible for parsing and interpreting the xml.
Because the LLM can output any text, the core application can't wait until the returnd text is valid xml.
To reliably indicate the end of text generation, we use the ASCII End of Transmission (EOT) character (ASCII code 4, `\u0004`).
This character was chosen because:
- It's specifically designed for this purpose in telecommunications protocols
- It should not appear in normal generated text
- It's a single byte, making it efficient to process
- It's standard across all platforms
**Example output from LLM engine subprocess:**
```
<reasoning>
I should check the current state of the system and see if there are any pending tasks.
</reasoning>\u0004
```
The communication protocol between the SIA agent and LLM engine subprocesses has been designed with simplicity as the primary goal.
Opting for a minimal approach that:
- Keeps complexity on the agent side, not the llm engine
- Uses familiar XML format for inputs to align with SIA's existing patterns
- Utilizes standard ASCII chars to indicate EOT
### Core Components
#### Agent Architecture
#### Agent
The core of SIA is the agent, which exists in two variants:
- ProceduralAgent: Runs in a simple loop, processing context and executing actions directly
- WebAgent: Uses a state machine to allow human intervention and feedback through a web interface
- ProceduralAgent: Runs in a simple state machine, processing context and executing actions directly
- WebAgent: Gives more control on when to change state and allows human intervention and feedback through a web interface
Both agent types share common components:
- WorkingMemory: Maintains the current state through a collection of entries and system metrics
- ResponseParser: Converts LLM output into commands or entries
- XMLValidator: Validates responses against a schema
- IOBuffer: Handles input/output operations in an agent-appropriate way
- WorkingMemory
- ResponseParser
- IterationLogger
- IOBuffer
Interaction with these components and other shared behaviour is handled in BaseAgent.
#### Working Memory
The working memory stores the current state of the system through different types of entries:
@@ -205,7 +374,7 @@ The working memory stores the current state of the system through different type
- IOEntry: Input/output operations
Each entry can be serialized to XML for inclusion in the LLM context.
Working memory is cleaned through explicit delete commands or when context size limits are reached.
Working memory is cleaned through explicit delete commands issued by the LLM.
#### Command Processing
SIA distinguishes between two types of LLM outputs:
@@ -213,13 +382,14 @@ SIA distinguishes between two types of LLM outputs:
- DeleteCommand: Removes entries from working memory
- StopCommand: Terminates the agent
2. Entries: Records that become part of working memory
- Created from script executions, IO operations, reasoning, or errors
- Persist until explicitly deleted or context limits are reached
- Stay in the context until explicitly deleted
- May execute once, each iteration or not at all depending on entry type
#### IO Handling
IO operations are abstracted through an IOBuffer interface:
- StandardIOBuffer: Direct access to system stdin/stdout
- WebIOBuffer: Buffer for web interface communication
This abstraction allows the ResponseParser to generate consistent IOEntries regardless of agent type.
### Processing Flow
@@ -284,9 +454,8 @@ classDiagram
}
class LLMEngine {
+LLMEngine(model_path str)
+set_model_path(model_path str) void
+infer(system_prompt str, main_context str) Iterator~str~
+LLMEngine(executable_path str)
+infer(system_prompt str, main_context str, prefix str) Iterator~str~
}
class BaseAgent {

View File

@@ -17,7 +17,6 @@ from .web.websockets import Websockets
from .web_agent import WebAgent
from .web_io_buffer import WebIOBuffer
from .working_memory import WorkingMemory
from .xml_validator import XMLValidator
class Main:
@classmethod
@@ -81,7 +80,6 @@ class Main:
working_memory=self._working_memory,
metrics=SystemMetrics(),
llms=self._llms,
validator=XMLValidator(self._action_schema),
parser=ResponseParser(config.work_dir, self._io_buffer),
iteration_logger=IterationLogger(self._config.iterations_dir, self._system_prompt, self._action_schema),
)

View File

@@ -6,7 +6,6 @@ from .response_parser import ResponseParser
from .system_metrics import SystemMetrics
from .util import pretty_print_element
from .working_memory import WorkingMemory
from .xml_validator import XMLValidator
class BaseAgent(ABC):
"""
@@ -22,7 +21,6 @@ class BaseAgent(ABC):
action_schema: str,
working_memory: WorkingMemory,
metrics: SystemMetrics,
validator: XMLValidator,
parser: ResponseParser,
):
"""
@@ -32,7 +30,6 @@ class BaseAgent(ABC):
self._action_schema = action_schema
self._working_memory = working_memory
self._metrics = metrics
self._validator = validator
self._parser = parser
@property

View File

@@ -14,7 +14,6 @@ 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()
@@ -28,7 +27,6 @@ class WebAgent(BaseAgent):
working_memory: WorkingMemory,
metrics: SystemMetrics,
llms: Dict[str, LlmEngine],
validator: XMLValidator,
parser: ResponseParser,
iteration_logger: IterationLogger,
):
@@ -37,7 +35,6 @@ class WebAgent(BaseAgent):
action_schema,
working_memory,
metrics,
validator,
parser
)
self._llms = llms

View File

@@ -1,97 +0,0 @@
import xml.etree.ElementTree as ET
from typing import Optional, Set
class XMLValidator:
"""
Validates XML content against a schema.
Attributes:
_schema: The parsed XML schema to validate against
_valid_root_elements: Set of valid root element names from schema
"""
def __init__(self, schema: str):
"""
Initialize validator with XML schema.
Args:
schema: XML schema string
"""
# Register namespace used in schema
ET.register_namespace('xs', 'http://www.w3.org/2001/XMLSchema')
try:
# Parse schema
self._schema = ET.fromstring(schema.strip())
# Extract valid root elements
ns = {'xs': 'http://www.w3.org/2001/XMLSchema'}
elements = self._schema.findall(".//xs:element", ns)
self._valid_root_elements = {elem.get('name') for elem in elements if elem.get('name')}
except ET.ParseError as e:
raise ValueError(f"Invalid schema: {e}")
def validate(self, xml: str) -> Optional[str]:
"""
Validate XML content against the schema.
Args:
xml: XML string to validate
Returns:
str: Error message if validation fails, None if validation succeeds
"""
try:
# Parse XML
root = ET.fromstring(xml.strip())
# Check root element is valid
if root.tag not in self._valid_root_elements:
return f"Invalid root element: {root.tag}. Expected one of: {sorted(self._valid_root_elements)}"
# Get schema definition for this element
ns = {'xs': 'http://www.w3.org/2001/XMLSchema'}
element_schema = self._schema.find(f".//xs:element[@name='{root.tag}']", ns)
if element_schema is None:
return f"Schema definition not found for element: {root.tag}"
# Validate attributes if complex type defined
complex_type = element_schema.find('xs:complexType', ns)
if complex_type is not None:
# Check required attributes
for attr in complex_type.findall('.//xs:attribute[@use="required"]', ns):
attr_name = attr.get('name')
if attr_name not in root.attrib:
return f"Missing required attribute '{attr_name}' on element '{root.tag}'"
# Check attribute types
for attr_name, attr_value in root.attrib.items():
attr_schema = complex_type.find(f'.//xs:attribute[@name="{attr_name}"]', ns)
if attr_schema is None:
return f"Unexpected attribute '{attr_name}' on element '{root.tag}'"
attr_type = attr_schema.get('type')
if attr_type == 'xs:string':
continue # All string values are valid
elif attr_type == 'xs:integer':
try:
int(attr_value)
except ValueError:
return f"Invalid integer value '{attr_value}' for attribute '{attr_name}'"
return None # Validation successful
except ET.ParseError as e:
return f"Invalid XML: {e}"
except Exception as e:
return f"Validation error: {e}"
def get_valid_root_elements(self) -> Set[str]:
"""
Get set of valid root element names from schema.
Returns:
Set[str]: Set of valid root element names
"""
return self._valid_root_elements.copy()