Response parser and split up class diagrams
This commit is contained in:
407
readme.md
407
readme.md
@@ -267,7 +267,100 @@ This architecture allows for:
|
|||||||
- Human-in-the-loop operation for testing and development
|
- Human-in-the-loop operation for testing and development
|
||||||
- Collection of human feedback for reinforcement learning
|
- Collection of human feedback for reinforcement learning
|
||||||
|
|
||||||
### Standard Agent Flow
|
### Diagrams
|
||||||
|
|
||||||
|
#### Core classes
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class SystemMetrics {
|
||||||
|
+SystemMetrics(sample_interval float)
|
||||||
|
+generate_context(context_usage float) ElementTree
|
||||||
|
+stop() void
|
||||||
|
-monitor_loop() void
|
||||||
|
}
|
||||||
|
|
||||||
|
class LLMEngine {
|
||||||
|
+LLMEngine(model_path str)
|
||||||
|
+set_model_path(model_path str) void
|
||||||
|
+infer(system_prompt str, main_context str) Iterator~str~
|
||||||
|
}
|
||||||
|
|
||||||
|
class BaseAgent {
|
||||||
|
<<abstract>>
|
||||||
|
#working_memory: WorkingMemory
|
||||||
|
#metrics: SystemMetrics
|
||||||
|
#llm: LLMEngine
|
||||||
|
#parser: ResponseParser
|
||||||
|
#validator: XMLValidator
|
||||||
|
#io_buffer: IOBuffer
|
||||||
|
|
||||||
|
#compile_context() str
|
||||||
|
}
|
||||||
|
|
||||||
|
class WorkingMemory {
|
||||||
|
-entries: List~Entry~
|
||||||
|
|
||||||
|
+WorkingMemory()
|
||||||
|
+add_entry(entry Entry) void
|
||||||
|
+remove_entry(id str) void
|
||||||
|
+clear() void
|
||||||
|
+get_entry(id str) Optional~Entry~
|
||||||
|
+get_entries() List~Entry~
|
||||||
|
+get_entries_count() int
|
||||||
|
+get_entries_by_type(type Type) List~Entry~
|
||||||
|
+update() void
|
||||||
|
+generate_context() List~ElementTree~
|
||||||
|
}
|
||||||
|
|
||||||
|
class XMLValidator {
|
||||||
|
+XMLValidator(schema str)
|
||||||
|
+validate(xml str) Optional~str~
|
||||||
|
+get_valid_root_elements() Set~str~
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResponseParser {
|
||||||
|
-io_buffer: IOBuffer
|
||||||
|
|
||||||
|
+ResponseParser(io_buffer IOBuffer)
|
||||||
|
+parse(xml str) Union~Command, Entry~
|
||||||
|
}
|
||||||
|
|
||||||
|
class Entry {
|
||||||
|
<<abstract>>
|
||||||
|
+id: str
|
||||||
|
+timestamp: datetime
|
||||||
|
|
||||||
|
+Entry(id str, timestamp datetime)
|
||||||
|
+update() void*
|
||||||
|
+generate_context() ElementTree*
|
||||||
|
+cleanup() void*
|
||||||
|
}
|
||||||
|
|
||||||
|
class IOBuffer {
|
||||||
|
<<interface>>
|
||||||
|
+read() str*
|
||||||
|
+write(content str) void*
|
||||||
|
+buffer_length() int*
|
||||||
|
}
|
||||||
|
|
||||||
|
class Command {
|
||||||
|
<<abstract>>
|
||||||
|
+execute(memory WorkingMemory) CommandResult*
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemMetrics "1" --* "1" BaseAgent
|
||||||
|
LLMEngine "1" --* "1" BaseAgent
|
||||||
|
XMLValidator "1" --* "1" BaseAgent
|
||||||
|
BaseAgent "1" *-- "1" IOBuffer
|
||||||
|
BaseAgent "1" *-- "1" WorkingMemory
|
||||||
|
BaseAgent "1" *-- "1" ResponseParser
|
||||||
|
WorkingMemory "1" *-- "*" Entry
|
||||||
|
ResponseParser ..> Entry
|
||||||
|
ResponseParser ..> Command
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Standard Agent Flow
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
stateDiagram-v2
|
stateDiagram-v2
|
||||||
@@ -287,7 +380,74 @@ stateDiagram-v2
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Web Agent Flow
|
#### Web Agent
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class BaseAgent {
|
||||||
|
<<abstract>>
|
||||||
|
#working_memory: WorkingMemory
|
||||||
|
#metrics: SystemMetrics
|
||||||
|
#llm: LLMEngine
|
||||||
|
#parser: ResponseParser
|
||||||
|
#validator: XMLValidator
|
||||||
|
#io_buffer: IOBuffer
|
||||||
|
|
||||||
|
#compile_context() str
|
||||||
|
}
|
||||||
|
|
||||||
|
class StandardAgent {
|
||||||
|
+StandardAgent(model_path str, system_prompt str, action_schema str)
|
||||||
|
+run() void
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebAgent {
|
||||||
|
-current_state: WebAgentState
|
||||||
|
|
||||||
|
+WebAgent(model_path str, system_prompt str, action_schema str)
|
||||||
|
+get_current_state() WebAgentState
|
||||||
|
+proceed() void
|
||||||
|
+add_state_change_handler(handler func) void
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebAgentState {
|
||||||
|
<<enumeration>>
|
||||||
|
UPDATE
|
||||||
|
CONTEXT_APPROVAL
|
||||||
|
INFERENCE
|
||||||
|
RESPONSE_APPROVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebServer {
|
||||||
|
-agent: WebAgent
|
||||||
|
-clients: List~WebSocket~
|
||||||
|
|
||||||
|
+WebServer(agent WebAgent)
|
||||||
|
-broadcast_state_change() void
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebIOBuffer {
|
||||||
|
-stdin_buffer: str
|
||||||
|
-stdout_buffer: str
|
||||||
|
|
||||||
|
+WebIOBuffer()
|
||||||
|
+read() str
|
||||||
|
+write(content str) void
|
||||||
|
+buffer_length() int
|
||||||
|
+append_stdin(content str) void
|
||||||
|
+get_stdout() str
|
||||||
|
+clear_stdout() void
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseAgent <|-- WebAgent
|
||||||
|
BaseAgent <|-- StandardAgent
|
||||||
|
|
||||||
|
WebServer "1" *-- "1" WebIOBuffer
|
||||||
|
WebServer "1" *-- "1" WebAgent
|
||||||
|
WebAgent "1" *-- "1" WebAgentState
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Web Agent Flow
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
stateDiagram-v2
|
stateDiagram-v2
|
||||||
@@ -314,226 +474,169 @@ stateDiagram-v2
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Class Diagram
|
#### Entry classes
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
classDiagram
|
classDiagram
|
||||||
direction LR
|
|
||||||
class BaseAgent {
|
|
||||||
<<abstract>>
|
|
||||||
#working_memory WorkingMemory
|
|
||||||
#metrics SystemMetrics
|
|
||||||
#llm LLMEngine
|
|
||||||
#parser ResponseParser
|
|
||||||
#validator XMLValidator
|
|
||||||
#io_buffer IOBuffer
|
|
||||||
#compile_context() str
|
|
||||||
}
|
|
||||||
|
|
||||||
class LLMEngine {
|
|
||||||
+LLMEngine(model_path str)
|
|
||||||
+set_model_path(model_path str)
|
|
||||||
+inference(context str) Iterator~str~
|
|
||||||
}
|
|
||||||
|
|
||||||
class StandardAgent {
|
|
||||||
+run()
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebAgent {
|
|
||||||
-current_state WebAgentState
|
|
||||||
+get_current_state() WebAgentState
|
|
||||||
+proceed()
|
|
||||||
+add_state_change_handler(handler func())
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebAgentState {
|
|
||||||
<<enumeration>>
|
|
||||||
UPDATE
|
|
||||||
CONTEXT_APPROVAL
|
|
||||||
INFERENCE
|
|
||||||
RESPONSE_APPROVAL
|
|
||||||
}
|
|
||||||
|
|
||||||
class WorkingMemory {
|
|
||||||
+WorkingMemory()
|
|
||||||
-entries List~Entry~
|
|
||||||
+add_entry(entry Entry)
|
|
||||||
+remove_entry(id str)
|
|
||||||
+update()
|
|
||||||
+generate_context() List~ElementTree~
|
|
||||||
}
|
|
||||||
|
|
||||||
class SystemMetrics {
|
|
||||||
+generate_context(entries List~ElementTree~) ElementTree
|
|
||||||
}
|
|
||||||
|
|
||||||
class XMLValidator {
|
|
||||||
-schema ElementTree
|
|
||||||
+XMLValidator(schema str)
|
|
||||||
+validate(xml str) Optional~str~
|
|
||||||
}
|
|
||||||
|
|
||||||
class ResponseParser {
|
|
||||||
-io_buffer IOBuffer
|
|
||||||
+ResponseParser(io_buffer IOBuffer)
|
|
||||||
+parse(xml str) Command | Entry
|
|
||||||
}
|
|
||||||
|
|
||||||
class Entry {
|
class Entry {
|
||||||
<<abstract>>
|
<<abstract>>
|
||||||
+id str
|
+id: str
|
||||||
+timestamp datetime
|
+timestamp: datetime
|
||||||
|
|
||||||
+Entry(id str, timestamp datetime)
|
+Entry(id str, timestamp datetime)
|
||||||
+update()*
|
+update() void*
|
||||||
+generate_context() ElementTree*
|
+generate_context() ElementTree*
|
||||||
|
+cleanup() void*
|
||||||
}
|
}
|
||||||
|
|
||||||
class SingleShotEntry {
|
class SingleShotEntry {
|
||||||
+script str
|
+script: str
|
||||||
+stdout str
|
+stdout: str
|
||||||
+stderr str
|
+stderr: str
|
||||||
+exit_code int
|
+exit_code: Optional~int~
|
||||||
|
|
||||||
+SingleShotEntry(script str, id str, timestamp datetime)
|
+SingleShotEntry(script str, id str, timestamp datetime)
|
||||||
+update()
|
+update() void
|
||||||
+generate_context() ElementTree
|
+generate_context() ElementTree
|
||||||
}
|
}
|
||||||
|
|
||||||
class RepeatEntry {
|
class RepeatEntry {
|
||||||
+script str
|
+script: str
|
||||||
+stdout str
|
+stdout: str
|
||||||
+stderr str
|
+stderr: str
|
||||||
+exit_code int
|
+exit_code: Optional~int~
|
||||||
|
|
||||||
+RepeatEntry(script str, id str, timestamp datetime)
|
+RepeatEntry(script str, id str, timestamp datetime)
|
||||||
+update()
|
+update() void
|
||||||
+generate_context() ElementTree
|
+generate_context() ElementTree
|
||||||
}
|
}
|
||||||
|
|
||||||
class BackgroundEntry {
|
class BackgroundEntry {
|
||||||
+script str
|
+script: str
|
||||||
+stdout str
|
+stdout: str
|
||||||
+stderr str
|
+stderr: str
|
||||||
+process Process
|
+process: Optional~Process~
|
||||||
|
-accumulated_stdout: str
|
||||||
|
-accumulated_stderr: str
|
||||||
|
-exit_code: Optional~int~
|
||||||
|
|
||||||
+BackgroundEntry(script str, id str, timestamp datetime)
|
+BackgroundEntry(script str, id str, timestamp datetime)
|
||||||
+update()
|
+update() void
|
||||||
+generate_context() ElementTree
|
+generate_context() ElementTree
|
||||||
|
+cleanup() void
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReasoningEntry {
|
class ReasoningEntry {
|
||||||
+content str
|
+content: str
|
||||||
|
|
||||||
+ReasoningEntry(content str, id str, timestamp datetime)
|
+ReasoningEntry(content str, id str, timestamp datetime)
|
||||||
+update()
|
+update() void
|
||||||
+generate_context() ElementTree
|
+generate_context() ElementTree
|
||||||
}
|
}
|
||||||
|
|
||||||
class ParseErrorEntry {
|
class ParseErrorEntry {
|
||||||
+content str
|
+content: str
|
||||||
+error str
|
+error: str
|
||||||
|
|
||||||
+ParseErrorEntry(content str, error str, id str, timestamp datetime)
|
+ParseErrorEntry(content str, error str, id str, timestamp datetime)
|
||||||
+update()
|
+update() void
|
||||||
+generate_context() ElementTree
|
+generate_context() ElementTree
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReadEntry {
|
class ReadEntry {
|
||||||
+content str
|
+content: str
|
||||||
|
+io_buffer: IOBuffer
|
||||||
|
|
||||||
+ReadEntry(io_buffer IOBuffer, id str, timestamp datetime)
|
+ReadEntry(io_buffer IOBuffer, id str, timestamp datetime)
|
||||||
+update()
|
+update() void
|
||||||
+generate_context() ElementTree
|
+generate_context() ElementTree
|
||||||
}
|
}
|
||||||
|
|
||||||
class WriteEntry {
|
class WriteEntry {
|
||||||
+content str
|
+content: str
|
||||||
|
+io_buffer: IOBuffer
|
||||||
|
|
||||||
+WriteEntry(content str, io_buffer IOBuffer, id str, timestamp datetime)
|
+WriteEntry(content str, io_buffer IOBuffer, id str, timestamp datetime)
|
||||||
+update()
|
+update() void
|
||||||
+generate_context() ElementTree
|
+generate_context() ElementTree
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ReasoningEntry --|> Entry
|
||||||
|
ParseErrorEntry --|> Entry
|
||||||
|
ReadEntry --|> Entry
|
||||||
|
Entry <|-- WriteEntry
|
||||||
|
Entry <|-- SingleShotEntry
|
||||||
|
Entry <|-- RepeatEntry
|
||||||
|
Entry <|-- BackgroundEntry
|
||||||
|
```
|
||||||
|
|
||||||
|
#### IO Buffer classes
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
class IOBuffer {
|
class IOBuffer {
|
||||||
<<interface>>
|
<<interface>>
|
||||||
+read() str*
|
+read() str*
|
||||||
+write(content str)*
|
+write(content str) void*
|
||||||
+buffer_length() int*
|
+buffer_length() int*
|
||||||
}
|
}
|
||||||
|
|
||||||
class StandardIOBuffer {
|
class StandardIOBuffer {
|
||||||
+StandardIOBuffer()
|
+StandardIOBuffer()
|
||||||
+read() str
|
+read() str
|
||||||
+write(content str)
|
+write(content str) void
|
||||||
+buffer_length() int
|
+buffer_length() int
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebIOBuffer {
|
class WebIOBuffer {
|
||||||
-stdin_buffer str
|
-stdin_buffer: str
|
||||||
-stdout_buffer str
|
-stdout_buffer: str
|
||||||
|
|
||||||
+WebIOBuffer()
|
+WebIOBuffer()
|
||||||
+read() str
|
+read() str
|
||||||
+write(content str)
|
+write(content str) void
|
||||||
+buffer_length() int
|
+buffer_length() int
|
||||||
+append_stdin(content str)
|
+append_stdin(content str) void
|
||||||
+get_stdout() str
|
+get_stdout() str
|
||||||
+clear_stdout()
|
+clear_stdout() void
|
||||||
|
}
|
||||||
|
|
||||||
|
IOBuffer <|.. WebIOBuffer
|
||||||
|
IOBuffer <|.. StandardIOBuffer
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Command classes
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
direction LR
|
||||||
|
class Command {
|
||||||
|
<<abstract>>
|
||||||
|
+execute(memory WorkingMemory) CommandResult*
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeleteCommand {
|
||||||
|
+DeleteCommand(id str)
|
||||||
|
+execute(memory WorkingMemory) CommandResult
|
||||||
|
}
|
||||||
|
|
||||||
|
class StopCommand {
|
||||||
|
+StopCommand()
|
||||||
|
+execute(memory WorkingMemory) CommandResult
|
||||||
}
|
}
|
||||||
|
|
||||||
class CommandResult {
|
class CommandResult {
|
||||||
+should_stop bool
|
+message: str
|
||||||
+success bool
|
+success: bool
|
||||||
+message str
|
+should_stop: bool
|
||||||
+CommandResult(should_stop bool, success bool, message str)
|
|
||||||
|
+CommandResult(message str, success bool, should_stop bool)
|
||||||
+static success() CommandResult
|
+static success() CommandResult
|
||||||
+static failure(message str) CommandResult
|
+static failure(message str) CommandResult
|
||||||
+static stop() CommandResult
|
+static stop() CommandResult
|
||||||
}
|
}
|
||||||
|
|
||||||
class Command {
|
|
||||||
<<abstract>>
|
|
||||||
+execute(memory &WorkingMemory) CommandResult*
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeleteCommand {
|
|
||||||
+id str
|
|
||||||
+DeleteCommand(id str)
|
|
||||||
+execute(memory &WorkingMemory) CommandResult
|
|
||||||
}
|
|
||||||
|
|
||||||
class StopCommand {
|
|
||||||
+execute(memory &WorkingMemory) CommandResult
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebServer {
|
|
||||||
-agent WebAgent
|
|
||||||
-clients List~WebSocket~
|
|
||||||
+WebServer(agent WebAgent)
|
|
||||||
-broadcast_state_change()
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseAgent <|-- WebAgent
|
|
||||||
BaseAgent <|-- StandardAgent
|
|
||||||
BaseAgent "1" *-- "1" IOBuffer
|
|
||||||
BaseAgent "1" *-- "1" WorkingMemory
|
|
||||||
BaseAgent "1" *-- "1" SystemMetrics
|
|
||||||
BaseAgent "1" *-- "1" XMLValidator
|
|
||||||
BaseAgent "1" *-- "1" LLMEngine
|
|
||||||
BaseAgent "1" *-- "1" ResponseParser
|
|
||||||
|
|
||||||
WebServer "1" *-- "1" WebIOBuffer
|
|
||||||
WebServer "1" *-- "1" WebAgent
|
|
||||||
|
|
||||||
WebAgent "1" *-- "1" WebAgentState
|
|
||||||
|
|
||||||
WorkingMemory "1" *-- "*" Entry
|
|
||||||
|
|
||||||
Entry <|-- ReasoningEntry
|
|
||||||
Entry <|-- ParseErrorEntry
|
|
||||||
Entry <|-- ReadEntry
|
|
||||||
Entry <|-- WriteEntry
|
|
||||||
Entry <|-- SingleShotEntry
|
|
||||||
Entry <|-- RepeatEntry
|
|
||||||
Entry <|-- BackgroundEntry
|
|
||||||
|
|
||||||
Command <|-- DeleteCommand
|
Command <|-- DeleteCommand
|
||||||
Command <|-- StopCommand
|
Command <|-- StopCommand
|
||||||
Command -- CommandResult
|
Command -- CommandResult
|
||||||
|
|
||||||
IOBuffer <|.. WebIOBuffer
|
|
||||||
IOBuffer <|.. StandardIOBuffer
|
|
||||||
```
|
```
|
||||||
154
sia/response_parser.py
Normal file
154
sia/response_parser.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import uuid
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from .command import Command
|
||||||
|
from .delete_command import DeleteCommand
|
||||||
|
from .stop_command import StopCommand
|
||||||
|
|
||||||
|
from .entry import Entry
|
||||||
|
from .background_entry import BackgroundEntry
|
||||||
|
from .parse_error_entry import ParseErrorEntry
|
||||||
|
from .read_entry import ReadEntry
|
||||||
|
from .reasoning_entry import ReasoningEntry
|
||||||
|
from .repeat_entry import RepeatEntry
|
||||||
|
from .single_shot_entry import SingleShotEntry
|
||||||
|
from .write_entry import WriteEntry
|
||||||
|
|
||||||
|
class ResponseParser:
|
||||||
|
"""
|
||||||
|
Parses XML responses from the LLM into commands or entries.
|
||||||
|
|
||||||
|
The parser validates the XML structure and converts it into the appropriate
|
||||||
|
Command or Entry object based on the root element tag. Invalid input results
|
||||||
|
in ParseErrorEntry objects rather than raising exceptions.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
io_buffer: Buffer to use for IO operations
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, io_buffer):
|
||||||
|
"""
|
||||||
|
Initialize parser with IO buffer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
io_buffer: Buffer to use for IO operations
|
||||||
|
"""
|
||||||
|
self.io_buffer = io_buffer
|
||||||
|
|
||||||
|
def _extract_content(self, xml: str, root: ET.Element) -> str:
|
||||||
|
"""
|
||||||
|
Extract content from XML, handling CDATA sections and preserving special characters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
xml: Original XML string
|
||||||
|
root: Parsed XML element
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Extracted content with proper handling of CDATA and special characters
|
||||||
|
"""
|
||||||
|
if root.text is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Find opening tag end
|
||||||
|
tag_name = root.tag
|
||||||
|
tag_start = xml.find('<' + tag_name)
|
||||||
|
tag_end = xml.find('>', tag_start)
|
||||||
|
if tag_end == -1:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Check for self-closing tag
|
||||||
|
if xml[tag_end-1] == '/':
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Find closing tag start
|
||||||
|
close_tag = '</' + tag_name + '>'
|
||||||
|
close_start = xml.rfind(close_tag)
|
||||||
|
if close_start == -1:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Extract content
|
||||||
|
content = xml[tag_end+1:close_start]
|
||||||
|
|
||||||
|
# Handle CDATA sections
|
||||||
|
if content.startswith('<![CDATA[') and content.endswith(']]>'):
|
||||||
|
content = content[9:-3] # Remove CDATA markers
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def parse(self, xml: str) -> Union[Command, Entry]:
|
||||||
|
"""
|
||||||
|
Parse XML response into a Command or Entry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
xml: XML string to parse
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Command or Entry based on the XML content
|
||||||
|
ParseErrorEntry for any parsing or validation errors
|
||||||
|
"""
|
||||||
|
entry_id = str(uuid.uuid4())
|
||||||
|
timestamp = datetime.now()
|
||||||
|
|
||||||
|
# First try to parse the XML
|
||||||
|
try:
|
||||||
|
root = ET.fromstring(xml.strip())
|
||||||
|
except ET.ParseError as e:
|
||||||
|
return ParseErrorEntry(xml, f"Invalid XML: {str(e)}", entry_id, timestamp)
|
||||||
|
|
||||||
|
# Extract content preserving special characters and handling CDATA
|
||||||
|
content = self._extract_content(xml, root)
|
||||||
|
|
||||||
|
# Convert to appropriate type based on root tag
|
||||||
|
try:
|
||||||
|
if root.tag == 'delete':
|
||||||
|
# Extract target ID and create delete command
|
||||||
|
target_id = root.get('id')
|
||||||
|
if not target_id:
|
||||||
|
return ParseErrorEntry(xml, "Delete command missing required 'id' attribute", entry_id, timestamp)
|
||||||
|
return DeleteCommand(target_id)
|
||||||
|
|
||||||
|
elif root.tag == 'stop':
|
||||||
|
# Create stop command
|
||||||
|
return StopCommand()
|
||||||
|
|
||||||
|
elif root.tag == 'background':
|
||||||
|
# Create background script entry
|
||||||
|
if not content:
|
||||||
|
return ParseErrorEntry(xml, "Background entry missing script content", entry_id, timestamp)
|
||||||
|
return BackgroundEntry(content, entry_id, timestamp)
|
||||||
|
|
||||||
|
elif root.tag == 'repeat':
|
||||||
|
# Create repeat script entry
|
||||||
|
if not content:
|
||||||
|
return ParseErrorEntry(xml, "Repeat entry missing script content", entry_id, timestamp)
|
||||||
|
return RepeatEntry(content, entry_id, timestamp)
|
||||||
|
|
||||||
|
elif root.tag == 'single_shot':
|
||||||
|
# Create single shot script entry
|
||||||
|
if not content:
|
||||||
|
return ParseErrorEntry(xml, "Single shot entry missing script content", entry_id, timestamp)
|
||||||
|
return SingleShotEntry(content, entry_id, timestamp)
|
||||||
|
|
||||||
|
elif root.tag == 'reasoning':
|
||||||
|
# Create reasoning entry
|
||||||
|
if not content:
|
||||||
|
return ParseErrorEntry(xml, "Reasoning entry missing content", entry_id, timestamp)
|
||||||
|
return ReasoningEntry(content, entry_id, timestamp)
|
||||||
|
|
||||||
|
elif root.tag == 'read_stdin':
|
||||||
|
# Create read entry - no content required
|
||||||
|
return ReadEntry(self.io_buffer, entry_id, timestamp)
|
||||||
|
|
||||||
|
elif root.tag == 'write_stdout':
|
||||||
|
# Create write entry
|
||||||
|
if not content:
|
||||||
|
return ParseErrorEntry(xml, "Write entry missing content", entry_id, timestamp)
|
||||||
|
return WriteEntry(content, self.io_buffer, entry_id, timestamp)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return ParseErrorEntry(xml, f"Unknown root element: {root.tag}", entry_id, timestamp)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return ParseErrorEntry(xml, f"Error parsing response: {str(e)}", entry_id, timestamp)
|
||||||
122
sia/system_metrics.py
Normal file
122
sia/system_metrics.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
class SystemMetrics:
|
||||||
|
"""
|
||||||
|
Tracks system metrics including CPU, GPU, memory and disk usage.
|
||||||
|
Maintains rolling averages for CPU and GPU between context generations.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
_stop_event: Threading event to signal monitor thread to stop
|
||||||
|
_monitor_thread: Background thread that collects usage data
|
||||||
|
_cpu_samples: List of CPU usage samples since last context
|
||||||
|
_gpu_samples: List of GPU usage samples since last context
|
||||||
|
_metrics_lock: Lock for thread-safe access to samples
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, sample_interval: float = 0.1):
|
||||||
|
"""
|
||||||
|
Initialize system metrics monitoring.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_interval: Seconds between usage samples
|
||||||
|
"""
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
self._cpu_samples = []
|
||||||
|
self._gpu_samples = []
|
||||||
|
self._metrics_lock = threading.Lock()
|
||||||
|
self._sample_interval = sample_interval
|
||||||
|
|
||||||
|
# Start monitoring thread
|
||||||
|
self._monitor_thread = threading.Thread(
|
||||||
|
target=self._monitor_loop,
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
self._monitor_thread.start()
|
||||||
|
|
||||||
|
def _get_gpu_usage(self) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
Get current GPU usage percentage.
|
||||||
|
Returns None if GPU monitoring not available.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import pynvml
|
||||||
|
pynvml.nvmlInit()
|
||||||
|
handle = pynvml.nvmlDeviceGetHandleByIndex(0)
|
||||||
|
info = pynvml.nvmlDeviceGetUtilizationRates(handle)
|
||||||
|
pynvml.nvmlShutdown()
|
||||||
|
return info.gpu
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _monitor_loop(self):
|
||||||
|
"""
|
||||||
|
Background loop that collects system usage samples.
|
||||||
|
Runs until stop event is set.
|
||||||
|
"""
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
with self._metrics_lock:
|
||||||
|
# Get CPU usage across all cores
|
||||||
|
self._cpu_samples.append(psutil.cpu_percent())
|
||||||
|
|
||||||
|
# Get GPU usage if available
|
||||||
|
gpu_usage = self._get_gpu_usage()
|
||||||
|
if gpu_usage is not None:
|
||||||
|
self._gpu_samples.append(gpu_usage)
|
||||||
|
|
||||||
|
time.sleep(self._sample_interval)
|
||||||
|
|
||||||
|
def generate_context(self, context_usage: float) -> ET.Element:
|
||||||
|
"""
|
||||||
|
Generate XML context element with current system metrics.
|
||||||
|
Clears usage samples after generating averages.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context_usage: Current context size usage (0-1)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ET.Element: Context element with system metrics
|
||||||
|
"""
|
||||||
|
# Create context element
|
||||||
|
context = ET.Element("context")
|
||||||
|
|
||||||
|
# Add timestamp
|
||||||
|
context.set("time", time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()))
|
||||||
|
|
||||||
|
# Add usage metrics
|
||||||
|
with self._metrics_lock:
|
||||||
|
# CPU average (or 0 if no samples)
|
||||||
|
cpu_avg = round(sum(self._cpu_samples) / len(self._cpu_samples)) if self._cpu_samples else 0
|
||||||
|
context.set("cpu", str(cpu_avg))
|
||||||
|
self._cpu_samples.clear()
|
||||||
|
|
||||||
|
# GPU average (or 0 if no samples)
|
||||||
|
gpu_avg = round(sum(self._gpu_samples) / len(self._gpu_samples)) if self._gpu_samples else 0
|
||||||
|
context.set("gpu", str(gpu_avg))
|
||||||
|
self._gpu_samples.clear()
|
||||||
|
|
||||||
|
# Memory usage in bytes
|
||||||
|
memory = psutil.virtual_memory()
|
||||||
|
context.set("memory_used", str(memory.used))
|
||||||
|
context.set("memory_total", str(memory.total))
|
||||||
|
|
||||||
|
# Disk usage in bytes (root partition)
|
||||||
|
disk = psutil.disk_usage('/')
|
||||||
|
context.set("disk_used", str(disk.used))
|
||||||
|
context.set("disk_total", str(disk.total))
|
||||||
|
|
||||||
|
# Context usage (0-100)
|
||||||
|
context.set("context", str(round(context_usage * 100)))
|
||||||
|
|
||||||
|
# Standard input buffer size
|
||||||
|
context.set("stdin", "0") # Set by agent
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the monitoring thread and clean up."""
|
||||||
|
self._stop_event.set()
|
||||||
|
self._monitor_thread.join()
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import unittest
|
|
||||||
|
|
||||||
from itertools import tee
|
|
||||||
|
|
||||||
from . import test_data
|
|
||||||
|
|
||||||
from sia.llm_engine import LlmEngine
|
|
||||||
|
|
||||||
class LlmEngineTest(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.model_path = "/root/model"
|
|
||||||
|
|
||||||
def test_initialization(self):
|
|
||||||
llm_engine = LlmEngine(self.model_path)
|
|
||||||
self.assertIsInstance(llm_engine, LlmEngine)
|
|
||||||
|
|
||||||
def test_infer(self):
|
|
||||||
main_context = "This is a test"
|
|
||||||
llm_engine = LlmEngine(self.model_path)
|
|
||||||
tokens = llm_engine.infer(test_data.echo_system_prompt, main_context)
|
|
||||||
print_tokens, result_tokens = tee(tokens)
|
|
||||||
for token in print_tokens:
|
|
||||||
print(token, end="", flush=True)
|
|
||||||
result = ''.join(result_tokens)
|
|
||||||
self.assertEqual(result, f"{main_context}<test_tag>{main_context}</test_tag>")
|
|
||||||
215
test/response_parser_test.py
Normal file
215
test/response_parser_test.py
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import unittest
|
||||||
|
from datetime import datetime
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
from sia.command import Command
|
||||||
|
from sia.delete_command import DeleteCommand
|
||||||
|
from sia.stop_command import StopCommand
|
||||||
|
from sia.entry import Entry
|
||||||
|
from sia.background_entry import BackgroundEntry
|
||||||
|
from sia.parse_error_entry import ParseErrorEntry
|
||||||
|
from sia.read_entry import ReadEntry
|
||||||
|
from sia.reasoning_entry import ReasoningEntry
|
||||||
|
from sia.repeat_entry import RepeatEntry
|
||||||
|
from sia.single_shot_entry import SingleShotEntry
|
||||||
|
from sia.write_entry import WriteEntry
|
||||||
|
from sia.web_io_buffer import WebIOBuffer
|
||||||
|
from sia.response_parser import ResponseParser
|
||||||
|
|
||||||
|
class ResponseParserTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test cases with parser and IO buffer"""
|
||||||
|
self.io_buffer = WebIOBuffer()
|
||||||
|
self.parser = ResponseParser(self.io_buffer)
|
||||||
|
|
||||||
|
def test_delete_command(self):
|
||||||
|
"""Test parsing delete command"""
|
||||||
|
xml = '<delete id="test-id-123"/>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, DeleteCommand)
|
||||||
|
self.assertEqual(result.id, "test-id-123")
|
||||||
|
|
||||||
|
def test_delete_command_missing_id(self):
|
||||||
|
"""Test parsing delete command without ID returns error entry"""
|
||||||
|
xml = '<delete/>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, ParseErrorEntry)
|
||||||
|
self.assertEqual(result.content, xml)
|
||||||
|
self.assertIn("missing required 'id'", result.error)
|
||||||
|
|
||||||
|
def test_stop_command(self):
|
||||||
|
"""Test parsing stop command"""
|
||||||
|
xml = '<stop/>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, StopCommand)
|
||||||
|
|
||||||
|
def test_background_entry(self):
|
||||||
|
"""Test parsing background entry"""
|
||||||
|
xml = '<background>echo test</background>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, BackgroundEntry)
|
||||||
|
self.assertEqual(result.script, "echo test")
|
||||||
|
|
||||||
|
def test_background_entry_empty_script(self):
|
||||||
|
"""Test parsing background entry with empty script returns error entry"""
|
||||||
|
xml = '<background/>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, ParseErrorEntry)
|
||||||
|
self.assertEqual(result.content, xml)
|
||||||
|
self.assertIn("missing script content", result.error)
|
||||||
|
|
||||||
|
def test_repeat_entry(self):
|
||||||
|
"""Test parsing repeat entry"""
|
||||||
|
xml = '<repeat>ls -l</repeat>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, RepeatEntry)
|
||||||
|
self.assertEqual(result.script, "ls -l")
|
||||||
|
|
||||||
|
def test_repeat_entry_empty_script(self):
|
||||||
|
"""Test parsing repeat entry with empty script returns error entry"""
|
||||||
|
xml = '<repeat/>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, ParseErrorEntry)
|
||||||
|
self.assertEqual(result.content, xml)
|
||||||
|
self.assertIn("missing script content", result.error)
|
||||||
|
|
||||||
|
def test_single_shot_entry(self):
|
||||||
|
"""Test parsing single shot entry"""
|
||||||
|
xml = '<single_shot>echo "one time"</single_shot>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, SingleShotEntry)
|
||||||
|
self.assertEqual(result.script, 'echo "one time"')
|
||||||
|
|
||||||
|
def test_single_shot_entry_empty_script(self):
|
||||||
|
"""Test parsing single shot entry with empty script returns error entry"""
|
||||||
|
xml = '<single_shot/>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, ParseErrorEntry)
|
||||||
|
self.assertEqual(result.content, xml)
|
||||||
|
self.assertIn("missing script content", result.error)
|
||||||
|
|
||||||
|
def test_reasoning_entry(self):
|
||||||
|
"""Test parsing reasoning entry"""
|
||||||
|
xml = '<reasoning>test reasoning</reasoning>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, ReasoningEntry)
|
||||||
|
self.assertEqual(result.content, "test reasoning")
|
||||||
|
|
||||||
|
def test_reasoning_entry_empty_content(self):
|
||||||
|
"""Test parsing reasoning entry with empty content returns error entry"""
|
||||||
|
xml = '<reasoning/>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, ParseErrorEntry)
|
||||||
|
self.assertEqual(result.content, xml)
|
||||||
|
self.assertIn("missing content", result.error)
|
||||||
|
|
||||||
|
def test_read_stdin_entry(self):
|
||||||
|
"""Test parsing read stdin entry"""
|
||||||
|
xml = '<read_stdin/>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, ReadEntry)
|
||||||
|
self.assertEqual(result.io_buffer, self.io_buffer)
|
||||||
|
|
||||||
|
def test_write_stdout_entry(self):
|
||||||
|
"""Test parsing write stdout entry"""
|
||||||
|
xml = '<write_stdout>test output</write_stdout>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, WriteEntry)
|
||||||
|
self.assertEqual(result.content, "test output")
|
||||||
|
self.assertEqual(result.io_buffer, self.io_buffer)
|
||||||
|
|
||||||
|
def test_write_stdout_entry_empty_content(self):
|
||||||
|
"""Test parsing write stdout entry with empty content returns error entry"""
|
||||||
|
xml = '<write_stdout/>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, ParseErrorEntry)
|
||||||
|
self.assertEqual(result.content, xml)
|
||||||
|
self.assertIn("missing content", result.error)
|
||||||
|
|
||||||
|
def test_unknown_element(self):
|
||||||
|
"""Test parsing unknown element returns error entry"""
|
||||||
|
xml = '<unknown>test</unknown>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, ParseErrorEntry)
|
||||||
|
self.assertEqual(result.content, xml)
|
||||||
|
self.assertIn("Unknown root element", result.error)
|
||||||
|
|
||||||
|
def test_malformed_xml(self):
|
||||||
|
"""Test parsing malformed XML returns error entry"""
|
||||||
|
xml = '<unclosed>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, ParseErrorEntry)
|
||||||
|
self.assertEqual(result.content, xml)
|
||||||
|
self.assertIn("Invalid XML", result.error)
|
||||||
|
|
||||||
|
def test_entry_id_generation(self):
|
||||||
|
"""Test that unique IDs are generated for entries"""
|
||||||
|
xml1 = '<reasoning>test 1</reasoning>'
|
||||||
|
xml2 = '<reasoning>test 2</reasoning>'
|
||||||
|
|
||||||
|
result1 = self.parser.parse(xml1)
|
||||||
|
result2 = self.parser.parse(xml2)
|
||||||
|
|
||||||
|
self.assertNotEqual(result1.id, result2.id)
|
||||||
|
|
||||||
|
def test_whitespace_handling(self):
|
||||||
|
"""Test handling of whitespace in XML"""
|
||||||
|
xml = """
|
||||||
|
<reasoning>
|
||||||
|
test content
|
||||||
|
with multiple lines
|
||||||
|
</reasoning>
|
||||||
|
"""
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, ReasoningEntry)
|
||||||
|
self.assertTrue(result.content.strip())
|
||||||
|
|
||||||
|
def test_script_with_special_characters(self):
|
||||||
|
"""Test parsing script with special characters"""
|
||||||
|
script = 'echo "Special chars: \n\t\r\'\"\\"'
|
||||||
|
xml = f'<single_shot>{script}</single_shot>'
|
||||||
|
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
self.assertIsInstance(result, SingleShotEntry)
|
||||||
|
self.assertEqual(result.script, script)
|
||||||
|
|
||||||
|
def test_cdata_content(self):
|
||||||
|
"""Test parsing content with CDATA sections"""
|
||||||
|
xml = """
|
||||||
|
<reasoning><![CDATA[Test content with <special> & chars]]></reasoning>
|
||||||
|
"""
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
self.assertIsInstance(result, ReasoningEntry)
|
||||||
|
self.assertEqual(result.content, "Test content with <special> & chars")
|
||||||
|
|
||||||
|
def test_empty_element_with_attributes(self):
|
||||||
|
"""Test parsing empty element with attributes"""
|
||||||
|
xml = '<read_stdin id="123" timestamp="2024-01-01"/>'
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
self.assertIsInstance(result, ReadEntry)
|
||||||
|
|
||||||
|
def test_error_in_content_extraction(self):
|
||||||
|
"""Test handling errors during content extraction"""
|
||||||
|
xml = '<single_shot><![CDATA[test]]>' # Malformed CDATA
|
||||||
|
result = self.parser.parse(xml)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, ParseErrorEntry)
|
||||||
|
self.assertEqual(result.content, xml)
|
||||||
|
self.assertIn("Invalid XML", result.error)
|
||||||
186
test/system_metrics_test.py
Normal file
186
test/system_metrics_test.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import unittest
|
||||||
|
import time
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import psutil
|
||||||
|
import multiprocessing
|
||||||
|
import math
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
from sia.system_metrics import SystemMetrics
|
||||||
|
|
||||||
|
def cpu_load_process():
|
||||||
|
"""Helper process that generates CPU load"""
|
||||||
|
while True:
|
||||||
|
math.factorial(100000)
|
||||||
|
|
||||||
|
class SystemMetricsTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
"""Create metrics instance with faster sampling for tests"""
|
||||||
|
self.metrics = SystemMetrics(sample_interval=0.01)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Ensure metrics monitoring is stopped"""
|
||||||
|
self.metrics.stop()
|
||||||
|
|
||||||
|
def validate_timestamp(self, timestamp: str):
|
||||||
|
"""Validate timestamp format and reasonableness"""
|
||||||
|
try:
|
||||||
|
# Check format
|
||||||
|
self.assertRegex(
|
||||||
|
timestamp,
|
||||||
|
r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check timestamp is recent (within last minute)
|
||||||
|
time_parts = timestamp[:-1].split('T') # Remove Z
|
||||||
|
time_str = f"{time_parts[0]} {time_parts[1]}"
|
||||||
|
timestamp_time = time.strptime(time_str, "%Y-%m-%d %H:%M:%S")
|
||||||
|
now = time.gmtime()
|
||||||
|
|
||||||
|
# Convert to seconds since epoch for comparison
|
||||||
|
timestamp_secs = time.mktime(timestamp_time)
|
||||||
|
now_secs = time.mktime(now)
|
||||||
|
|
||||||
|
self.assertLess(abs(now_secs - timestamp_secs), 60)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.fail(f"Invalid timestamp format: {timestamp}")
|
||||||
|
|
||||||
|
def validate_memory_metrics(self, used: int, total: int):
|
||||||
|
"""Validate memory metrics are reasonable"""
|
||||||
|
# Check types
|
||||||
|
self.assertIsInstance(used, int)
|
||||||
|
self.assertIsInstance(total, int)
|
||||||
|
|
||||||
|
# Used memory should be positive and less than total
|
||||||
|
self.assertGreater(used, 0)
|
||||||
|
self.assertGreater(total, 0)
|
||||||
|
self.assertLessEqual(used, total)
|
||||||
|
|
||||||
|
# Compare with actual system memory
|
||||||
|
memory = psutil.virtual_memory()
|
||||||
|
self.assertEqual(total, memory.total)
|
||||||
|
# Used memory should be within 20% of current usage
|
||||||
|
# (allowing for normal fluctuation)
|
||||||
|
self.assertLess(abs(used - memory.used) / memory.total, 0.2)
|
||||||
|
|
||||||
|
def validate_disk_metrics(self, used: int, total: int):
|
||||||
|
"""Validate disk metrics are reasonable"""
|
||||||
|
# Check types
|
||||||
|
self.assertIsInstance(used, int)
|
||||||
|
self.assertIsInstance(total, int)
|
||||||
|
|
||||||
|
# Used space should be positive and less than total
|
||||||
|
self.assertGreater(used, 0)
|
||||||
|
self.assertGreater(total, 0)
|
||||||
|
self.assertLessEqual(used, total)
|
||||||
|
|
||||||
|
# Compare with actual disk usage
|
||||||
|
disk = psutil.disk_usage('/')
|
||||||
|
self.assertEqual(total, disk.total)
|
||||||
|
# Used space should match within 1% of actual usage
|
||||||
|
self.assertLess(abs(used - disk.used) / disk.total, 0.01)
|
||||||
|
|
||||||
|
def validate_usage_values(self, values: List[Tuple[str, int]]):
|
||||||
|
"""Validate usage percentage values are in valid range"""
|
||||||
|
for name, value in values:
|
||||||
|
self.assertIsInstance(value, int)
|
||||||
|
self.assertGreaterEqual(
|
||||||
|
value, 0,
|
||||||
|
f"{name} below valid range: {value}"
|
||||||
|
)
|
||||||
|
self.assertLessEqual(
|
||||||
|
value, 100,
|
||||||
|
f"{name} above valid range: {value}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_initial_context(self):
|
||||||
|
"""Test initial context generation"""
|
||||||
|
context = self.metrics.generate_context(0.5)
|
||||||
|
|
||||||
|
# Verify it's an XML element
|
||||||
|
self.assertIsInstance(context, ET.Element)
|
||||||
|
self.assertEqual(context.tag, "context")
|
||||||
|
|
||||||
|
# Validate timestamp
|
||||||
|
self.validate_timestamp(context.get("time"))
|
||||||
|
|
||||||
|
# Validate memory metrics
|
||||||
|
self.validate_memory_metrics(
|
||||||
|
int(context.get("memory_used")),
|
||||||
|
int(context.get("memory_total"))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate disk metrics
|
||||||
|
self.validate_disk_metrics(
|
||||||
|
int(context.get("disk_used")),
|
||||||
|
int(context.get("disk_total"))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate usage percentages
|
||||||
|
self.validate_usage_values([
|
||||||
|
("CPU", int(context.get("cpu"))),
|
||||||
|
("GPU", int(context.get("gpu"))),
|
||||||
|
("Context", int(context.get("context")))
|
||||||
|
])
|
||||||
|
|
||||||
|
# Validate stdin buffer size
|
||||||
|
self.assertEqual(context.get("stdin"), "0")
|
||||||
|
|
||||||
|
def test_multiple_context_generations(self):
|
||||||
|
"""Test multiple context generations have different timestamps"""
|
||||||
|
context1 = self.metrics.generate_context(0.5)
|
||||||
|
time.sleep(1)
|
||||||
|
context2 = self.metrics.generate_context(0.5)
|
||||||
|
|
||||||
|
self.assertNotEqual(
|
||||||
|
context1.get("time"),
|
||||||
|
context2.get("time")
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_cpu_usage_detection(self):
|
||||||
|
"""Test CPU usage increases under load"""
|
||||||
|
# Get initial CPU usage
|
||||||
|
context1 = self.metrics.generate_context(0.5)
|
||||||
|
initial_cpu = int(context1.get("cpu"))
|
||||||
|
|
||||||
|
# Generate CPU load in separate process
|
||||||
|
process = multiprocessing.Process(target=cpu_load_process)
|
||||||
|
process.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Wait for samples to accumulate
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Get CPU usage under load
|
||||||
|
context2 = self.metrics.generate_context(0.5)
|
||||||
|
load_cpu = int(context2.get("cpu"))
|
||||||
|
|
||||||
|
# CPU usage should increase
|
||||||
|
self.assertGreater(load_cpu, initial_cpu)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
process.terminate()
|
||||||
|
process.join()
|
||||||
|
|
||||||
|
def test_context_usage_reflection(self):
|
||||||
|
"""Test context usage parameter is reflected accurately"""
|
||||||
|
test_values = [0.0, 0.42, 0.8, 1.0]
|
||||||
|
|
||||||
|
for value in test_values:
|
||||||
|
context = self.metrics.generate_context(value)
|
||||||
|
self.assertEqual(
|
||||||
|
int(context.get("context")),
|
||||||
|
round(value * 100)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_cleanup(self):
|
||||||
|
"""Test monitoring stops properly"""
|
||||||
|
self.metrics.stop()
|
||||||
|
|
||||||
|
# Monitor thread should finish
|
||||||
|
self.assertFalse(self.metrics._monitor_thread.is_alive())
|
||||||
|
|
||||||
|
# Should still generate valid context after stopping
|
||||||
|
context = self.metrics.generate_context(0.5)
|
||||||
|
self.assertIsInstance(context, ET.Element)
|
||||||
Reference in New Issue
Block a user