6906 lines
229 KiB
Plaintext
6906 lines
229 KiB
Plaintext
Directory Tree:
|
|
.
|
|
|____.dockerignore
|
|
|____.env
|
|
|____.gitignore
|
|
|____action_schema.xsd
|
|
|____collect.txt
|
|
|____data
|
|
| |____iterations
|
|
| | |____iteration_20250109_190024_373.xml
|
|
| | |____iteration_20250109_190035_409.xml
|
|
| | |____iteration_20250109_190704_483.xml
|
|
| | |____iteration_20250109_190709_915.xml
|
|
| | |____iteration_20250109_190715_552.xml
|
|
| | |____iteration_20250109_190722_336.xml
|
|
| | |____iteration_20250109_190727_381.xml
|
|
| | |____iteration_20250109_190733_628.xml
|
|
| | |____iteration_20250109_190744_602.xml
|
|
| | |____iteration_20250109_190749_974.xml
|
|
| | |____iteration_20250109_190817_093.xml
|
|
| | |____iteration_20250109_190822_365.xml
|
|
| | |____iteration_20250109_190827_602.xml
|
|
| | |____iteration_20250109_190843_601.xml
|
|
| | |____iteration_20250109_190849_963.xml
|
|
| | |____iteration_20250109_190954_879.xml
|
|
| | |____iteration_20250109_190957_794.xml
|
|
| | |____iteration_20250109_191006_871.xml
|
|
| | |____iteration_20250109_191015_730.xml
|
|
| | |____iteration_20250109_191037_978.xml
|
|
| | |____iteration_20250109_191052_893.xml
|
|
| | |____iteration_20250109_191122_014.xml
|
|
| | |____iteration_20250109_191125_681.xml
|
|
| | |____iteration_20250109_191132_177.xml
|
|
| | |____iteration_20250109_191149_428.xml
|
|
| | |____iteration_20250109_191156_182.xml
|
|
| | |____iteration_20250109_191209_080.xml
|
|
| | |____iteration_20250109_191232_614.xml
|
|
| | |____iteration_20250109_191750_117.xml
|
|
| | |____iteration_20250109_191820_461.xml
|
|
| | |____iteration_20250109_191829_707.xml
|
|
| | |____iteration_20250109_191848_803.xml
|
|
| | |____iteration_20250109_191857_135.xml
|
|
| | |____iteration_20250109_191903_535.xml
|
|
| | |____iteration_20250109_191915_839.xml
|
|
| | |____iteration_20250109_191919_044.xml
|
|
| | |____iteration_20250109_191923_827.xml
|
|
| | |____iteration_20250109_191927_361.xml
|
|
| | |____iteration_20250109_191931_571.xml
|
|
| | |____iteration_20250109_191935_623.xml
|
|
| | |____iteration_20250109_191944_403.xml
|
|
| | |____iteration_20250109_191948_286.xml
|
|
| | |____iteration_20250109_191951_279.xml
|
|
| | |____iteration_20250109_191958_277.xml
|
|
| | |____iteration_20250109_192001_725.xml
|
|
| | |____iteration_20250109_192005_801.xml
|
|
| | |____iteration_20250109_192008_636.xml
|
|
| | |____iteration_20250109_192011_586.xml
|
|
| | |____iteration_20250109_192014_562.xml
|
|
| | |____iteration_20250109_192016_964.xml
|
|
| | |____iteration_20250109_192019_514.xml
|
|
| | |____iteration_20250109_192023_754.xml
|
|
| | |____iteration_20250109_192029_400.xml
|
|
| | |____iteration_20250109_192053_637.xml
|
|
| | |____iteration_20250109_192108_043.xml
|
|
| | |____iteration_20250109_192112_980.xml
|
|
| | |____iteration_20250109_192116_218.xml
|
|
| | |____iteration_20250109_192118_117.xml
|
|
| | |____iteration_20250109_192120_069.xml
|
|
| | |____iteration_20250109_192122_109.xml
|
|
| | |____iteration_20250109_192125_737.xml
|
|
| | |____iteration_20250109_192139_935.xml
|
|
| | |____iteration_20250109_192246_171.xml
|
|
| | |____iteration_20250109_192251_105.xml
|
|
| | |____iteration_20250109_192301_749.xml
|
|
| | |____iteration_20250109_192305_163.xml
|
|
| | |____iteration_20250109_192307_861.xml
|
|
| | |____iteration_20250109_192311_217.xml
|
|
| | |____iteration_20250109_192315_575.xml
|
|
| | |____iteration_20250109_192318_269.xml
|
|
| | |____iteration_20250109_192320_920.xml
|
|
| | |____iteration_20250109_192324_888.xml
|
|
| | |____iteration_20250109_192328_431.xml
|
|
| | |____iteration_20250109_192332_861.xml
|
|
| | |____iteration_20250109_192335_705.xml
|
|
| | |____iteration_20250109_192338_398.xml
|
|
| | |____iteration_20250109_192341_071.xml
|
|
| | |____iteration_20250109_192344_092.xml
|
|
| | |____iteration_20250109_192353_015.xml
|
|
| | |____iteration_20250109_192356_373.xml
|
|
| | |____iteration_20250109_192359_203.xml
|
|
| | |____iteration_20250109_192404_132.xml
|
|
| | |____iteration_20250109_192409_428.xml
|
|
| | |____iteration_20250109_192412_471.xml
|
|
| | |____iteration_20250109_192417_100.xml
|
|
| | |____iteration_20250109_192420_341.xml
|
|
| | |____iteration_20250109_192423_203.xml
|
|
| | |____iteration_20250109_192426_541.xml
|
|
| | |____iteration_20250109_192429_583.xml
|
|
| | |____iteration_20250109_192437_744.xml
|
|
| | |____iteration_20250109_192450_273.xml
|
|
| | |____iteration_20250109_192455_171.xml
|
|
| | |____iteration_20250109_192504_385.xml
|
|
| | |____iteration_20250109_192507_329.xml
|
|
| | |____iteration_20250109_192532_348.xml
|
|
| | |____iteration_20250109_192539_330.xml
|
|
| | |____iteration_20250109_192547_957.xml
|
|
| | |____iteration_20250109_192600_776.xml
|
|
| | |____iteration_20250109_192611_571.xml
|
|
| |____tasks
|
|
| | |____fix_unit_tests.txt
|
|
| | |____improve_sia.txt
|
|
| | |____kapla.txt
|
|
| |____user
|
|
|____Dockerfile
|
|
|____environment
|
|
|____iterations
|
|
|____mini_prompt.md
|
|
|____model
|
|
| |____config.json
|
|
| |____generation_config.json
|
|
| |____model.safetensors
|
|
| |____special_tokens_map.json
|
|
| |____tokenizer.json
|
|
| |____tokenizer_config.json
|
|
|____procedures
|
|
| |____filesystem_design
|
|
| | |____reasoning.md
|
|
| |____README.md
|
|
| |____self_improvement
|
|
| | |____reasoning.md
|
|
| |____tool_management
|
|
| | |____reasoning.md
|
|
| |____user_communication
|
|
| | |____description.md
|
|
| | |____procedure.md
|
|
| | |____reasoning.md
|
|
| |____using_procedures
|
|
| | |____procedure.md
|
|
| | |____reasoning.md
|
|
| |____version_control
|
|
| | |____reasoning.md
|
|
|____README.md
|
|
|____requirements.txt
|
|
|____scripts
|
|
| |____bootstrap.sh
|
|
| |____collect.sh
|
|
| |____container.sh
|
|
| |____restart.sh
|
|
| |____run.sh
|
|
| |____setup_binaries.py
|
|
| |____test.sh
|
|
|____setup.py
|
|
|____sia
|
|
| |____auto_approver.py
|
|
| |____base_agent.py
|
|
| |____command.py
|
|
| |____command_result.py
|
|
| |____config.py
|
|
| |____delete_command.py
|
|
| |____entry
|
|
| | |____background_entry.py
|
|
| | |____entry_factory.py
|
|
| | |____parse_error_entry.py
|
|
| | |____read_entry.py
|
|
| | |____reasoning_entry.py
|
|
| | |____repeat_entry.py
|
|
| | |____single_entry.py
|
|
| | |____write_entry.py
|
|
| | |______init__.py
|
|
| | |______pycache__
|
|
| | | |____background_entry.cpython-310.pyc
|
|
| | | |____entry_factory.cpython-310.pyc
|
|
| | | |____parse_error_entry.cpython-310.pyc
|
|
| | | |____read_entry.cpython-310.pyc
|
|
| | | |____reasoning_entry.cpython-310.pyc
|
|
| | | |____repeat_entry.cpython-310.pyc
|
|
| | | |____single_entry.cpython-310.pyc
|
|
| | | |____write_entry.cpython-310.pyc
|
|
| | | |______init__.cpython-310.pyc
|
|
| |____io_buffer.py
|
|
| |____iteration_logger.py
|
|
| |____iteration_parser.py
|
|
| |____llm_engine
|
|
| | |____deepseek_llm_engine.py
|
|
| | |____hf_llm_engine.py
|
|
| | |____local_llm_engine.py
|
|
| | |____mistral_llm_engine.py
|
|
| | |____openai_llm_engine.py
|
|
| | |______init__.py
|
|
| | |______pycache__
|
|
| | | |____deepseek_llm_engine.cpython-310.pyc
|
|
| | | |____hf_llm_engine.cpython-310.pyc
|
|
| | | |____local_llm_engine.cpython-310.pyc
|
|
| | | |____mistral_llm_engine.cpython-310.pyc
|
|
| | | |____openai_llm_engine.cpython-310.pyc
|
|
| | | |______init__.cpython-310.pyc
|
|
| |____response_parser.py
|
|
| |____standard_io_buffer.py
|
|
| |____stop_command.py
|
|
| |____system_metrics.py
|
|
| |____util.py
|
|
| |____web
|
|
| | |____api.py
|
|
| | |____auto_approver_websocket.py
|
|
| | |____context_websocket.py
|
|
| | |____llm_websocket.py
|
|
| | |____memory_websocket.py
|
|
| | |____static.py
|
|
| | |____stdout_websocket.py
|
|
| | |____token_websocket.py
|
|
| | |____util.py
|
|
| | |____websockts.py
|
|
| | |______pycache__
|
|
| | | |____api.cpython-310.pyc
|
|
| | | |____auto_approver_websocket.cpython-310.pyc
|
|
| | | |____context_websocket.cpython-310.pyc
|
|
| | | |____llm_websocket.cpython-310.pyc
|
|
| | | |____memory_websocket.cpython-310.pyc
|
|
| | | |____static.cpython-310.pyc
|
|
| | | |____stdout_websocket.cpython-310.pyc
|
|
| | | |____token_websocket.cpython-310.pyc
|
|
| | | |____util.cpython-310.pyc
|
|
| | | |____websockts.cpython-310.pyc
|
|
| |____web_agent.py
|
|
| |____web_io_buffer.py
|
|
| |____working_memory.py
|
|
| |____xml_validator.py
|
|
| |______init__.py
|
|
| |______main__.py
|
|
| |______pycache__
|
|
| | |____auto_approver.cpython-310.pyc
|
|
| | |____base_agent.cpython-310.pyc
|
|
| | |____command.cpython-310.pyc
|
|
| | |____command_result.cpython-310.pyc
|
|
| | |____config.cpython-310.pyc
|
|
| | |____delete_command.cpython-310.pyc
|
|
| | |____hf_llm_engine.cpython-310.pyc
|
|
| | |____io_buffer.cpython-310.pyc
|
|
| | |____iteration_logger.cpython-310.pyc
|
|
| | |____iteration_parser.cpython-310.pyc
|
|
| | |____llm_engine.cpython-310.pyc
|
|
| | |____local_llm_engine.cpython-310.pyc
|
|
| | |____mistral_llm_engine.cpython-310.pyc
|
|
| | |____openai_llm_engine.cpython-310.pyc
|
|
| | |____response_parser.cpython-310.pyc
|
|
| | |____stop_command.cpython-310.pyc
|
|
| | |____system_metrics.cpython-310.pyc
|
|
| | |____util.cpython-310.pyc
|
|
| | |____web_agent.cpython-310.pyc
|
|
| | |____web_io_buffer.cpython-310.pyc
|
|
| | |____working_memory.cpython-310.pyc
|
|
| | |____xml_validator.cpython-310.pyc
|
|
| | |______init__.cpython-310.pyc
|
|
| | |______main__.cpython-310.pyc
|
|
|____sia.egg-info
|
|
| |____dependency_links.txt
|
|
| |____entry_points.txt
|
|
| |____PKG-INFO
|
|
| |____requires.txt
|
|
| |____SOURCES.txt
|
|
| |____top_level.txt
|
|
|____system_prompt.md
|
|
|____tasks
|
|
|____test
|
|
| |____auto_approver_test.py
|
|
| |____background_entry_test.py
|
|
| |____base_agent_test.py
|
|
| |____delete_command_test.py
|
|
| |____local_llm_engine_test.py
|
|
| |____parse_error_entry_test.py
|
|
| |____read_entry_test.py
|
|
| |____reasoning_entry_test.py
|
|
| |____repeat_entry_test.py
|
|
| |____response_parser_test.py
|
|
| |____single_entry_test.py
|
|
| |____standard_io_buffer_test.py
|
|
| |____stop_command_test.py
|
|
| |____system_metrics_test.py
|
|
| |____test_data.py
|
|
| |____util_test.py
|
|
| |____web_agent_test.py
|
|
| |____web_io_buffer_test.py
|
|
| |____web_socket_manager_test.py
|
|
| |____working_memory_test.py
|
|
| |____write_entry_test.py
|
|
| |____xml_validator_test.py
|
|
| |______init__.py
|
|
|____tools
|
|
| |____itb
|
|
| | |____bin
|
|
| | | |____itb_click
|
|
| | | |____itb_cursor
|
|
| | | |____itb_input
|
|
| | | |____itb_navigate
|
|
| | | |____itb_refresh
|
|
| | | |____itb_screenshot
|
|
| | | |____itb_scroll
|
|
| | | |____itb_start
|
|
| | |____itb.egg-info
|
|
| | | |____dependency_links.txt
|
|
| | | |____PKG-INFO
|
|
| | | |____requires.txt
|
|
| | | |____SOURCES.txt
|
|
| | | |____top_level.txt
|
|
| | |____js
|
|
| | | |____dom_analyzer.js
|
|
| | | |____dom_analyzer.js.orig
|
|
| | | |____dom_analyzer.js.patch
|
|
| | | |____dom_analyzer.js.rej
|
|
| | | |____mouse.js
|
|
| | | |____viewport.js
|
|
| | |____README.md
|
|
| | |____requirements.txt
|
|
| | |____setup.py
|
|
| | |____social.html
|
|
| |____train
|
|
| | |____bin
|
|
| | | |____train_deepseek
|
|
| | | |____train_mistral
|
|
| | |____readme.md
|
|
| | |____requirements.txt
|
|
| | |____setup.py
|
|
| | |____train
|
|
| | | |____mistral_api.py
|
|
| | | |____unsloth_deepseek.py
|
|
| | | |____util.py
|
|
| | | |______init__.py
|
|
| | |____train.sh
|
|
|____training
|
|
| |____clean_start
|
|
| | |____iteration_20250116_134549_655.xml
|
|
| | |____iteration_20250116_134555_680.xml
|
|
| |____config.yaml
|
|
| |____delete_indicated_entries
|
|
| | |____iteration_20250116_141241_092.xml
|
|
| | |____iteration_20250116_141252_317.xml
|
|
| | |____iteration_20250116_141302_940.xml
|
|
| | |____iteration_20250116_141329_886.xml
|
|
| | |____iteration_20250116_141343_416.xml
|
|
| | |____iteration_20250116_141357_412.xml
|
|
| | |____iteration_20250116_141410_965.xml
|
|
| | |____iteration_20250116_141428_204.xml
|
|
| | |____iteration_20250116_141441_443.xml
|
|
| | |____iteration_20250116_141447_231.xml
|
|
| | |____iteration_20250116_141454_509.xml
|
|
| | |____iteration_20250116_141458_495.xml
|
|
| | |____iteration_20250116_141503_889.xml
|
|
| | |____iteration_20250116_141516_718.xml
|
|
| | |____iteration_20250116_141533_231.xml
|
|
| | |____iteration_20250116_141603_549.xml
|
|
| | |____iteration_20250116_141633_083.xml
|
|
| |____list_entries_to_delete
|
|
| | |____iteration_20250116_141227_271.xml
|
|
|____user
|
|
|____web
|
|
| |____.dockerignore
|
|
| |____.gitignore
|
|
| |____index.html
|
|
| |____package.json
|
|
| |____postcss.config.js
|
|
| |____src
|
|
| | |____components
|
|
| | | |____App.jsx
|
|
| | | |____editors
|
|
| | | | |____BackgroundEntryEditor.jsx
|
|
| | | | |____BaseEntryEditor.jsx
|
|
| | | | |____ContentEditor.jsx
|
|
| | | | |____DiffEditorWrapper.jsx
|
|
| | | | |____MemoryEditor.jsx
|
|
| | | | |____ParseErrorEntryEditor.jsx
|
|
| | | | |____ReadEntryEditor.jsx
|
|
| | | | |____ReasoningEntryEditor.jsx
|
|
| | | | |____RepeatEntryEditor.jsx
|
|
| | | | |____SingleEntryEditor.jsx
|
|
| | | | |____StandardEditor.jsx
|
|
| | | | |____WriteEntryEditor.jsx
|
|
| | | |____FloatingButton.jsx
|
|
| | | |____Header.jsx
|
|
| | | |____Sidebar.jsx
|
|
| | | |____ui
|
|
| | | | |____alert.jsx
|
|
| | | | |____button.jsx
|
|
| | | | |____card.jsx
|
|
| | | | |____input.jsx
|
|
| | | | |____scroll-area.jsx
|
|
| | | | |____select.jsx
|
|
| | | | |____switch.jsx
|
|
| | | | |____tabs.jsx
|
|
| | | | |____textarea.jsx
|
|
| | |____hooks
|
|
| | | |____useWebSocket.jsx
|
|
| | |____index.css
|
|
| | |____index.jsx
|
|
| |____tailwind.config.js
|
|
| |____vite.config.js
|
|
|
|
|
|
File Contents:
|
|
|
|
===== FILE: action_schema.xsd =====
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!--
|
|
Always answer with a single element.
|
|
-->
|
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
|
|
|
|
<!--
|
|
Delete command removes an entry from the context by its ID.
|
|
Use it to remove unnecessary items and stop background processes.
|
|
When you delete something, it is gone.
|
|
Make sure all important info is stored in files.
|
|
Example:
|
|
<delete id="1234567890"/>
|
|
-->
|
|
<xs:element name="delete">
|
|
<xs:complexType>
|
|
<xs:attribute name="id" type="xs:string" use="required"/>
|
|
</xs:complexType>
|
|
</xs:element>
|
|
|
|
<!--
|
|
Stop command terminates the agent gracefully.
|
|
For the main SIA instance this will trigger an update and restart.
|
|
For sub-instances this is the correct way to stop after all tasks are complete.
|
|
Example:
|
|
<stop id="1234567890"/>
|
|
-->
|
|
<xs:element name="stop">
|
|
<xs:complexType/>
|
|
</xs:element>
|
|
|
|
<!--
|
|
Single script that runs once and completes.
|
|
Output is stored in context until explicitly deleted.
|
|
Used for one-time operations like file manipulation.
|
|
Single scripts are limited to 1024 characters and 1 second timeout by default.
|
|
These limits can be changed with attributes.
|
|
Example:
|
|
<single>
|
|
ls /
|
|
</single>
|
|
-->
|
|
<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>
|
|
|
|
<!--
|
|
Repeat script runs each time the context is generated.
|
|
After a command is issued, all repeat scripts in context are run again.
|
|
Useful for monitoring changing files or viewing results immediately after changing a file.
|
|
Repeat scripts should execute quickly to avoid blocking the agent.
|
|
Repeat scripts are limited to 1024 characters and 1 second timeout by default.
|
|
These limits can be changed with attributes.
|
|
Example:
|
|
<repeat>
|
|
ls /
|
|
</repeat>
|
|
-->
|
|
<xs:element name="repeat">
|
|
<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>
|
|
|
|
<!--
|
|
As an agent it is important to reason about your actions and their results.
|
|
In a reasoning action you can write freeform text.
|
|
This is also stored in context until deleted.
|
|
Example:
|
|
<reasoning>
|
|
I should explore the file system for interesting files.
|
|
</reasoning>
|
|
-->
|
|
<xs:element name="reasoning">
|
|
<xs:complexType mixed="true">
|
|
<xs:sequence>
|
|
<xs:any minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
|
|
</xs:sequence>
|
|
</xs:complexType>
|
|
</xs:element>
|
|
|
|
<!--
|
|
Read all available text on stdin and store it in context.
|
|
Do this only if the context indicates there is data in the stdin buffer.
|
|
Example:
|
|
<read_stdin/>
|
|
-->
|
|
<xs:element name="read_stdin">
|
|
<xs:complexType/>
|
|
</xs:element>
|
|
|
|
<!--
|
|
Write to stdout.
|
|
This is your main way of contacting the user.
|
|
Make sure you have properly reasoned about what to say and if it is necessary before issuing a write_stdout command.
|
|
Example:
|
|
<write_stdout>
|
|
Hello world!
|
|
</write_stdout>
|
|
-->
|
|
<xs:element name="write_stdout">
|
|
<xs:complexType mixed="true">
|
|
<xs:sequence>
|
|
<xs:any minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
|
|
</xs:sequence>
|
|
</xs:complexType>
|
|
</xs:element>
|
|
</xs:schema>
|
|
|
|
===== FILE: Dockerfile =====
|
|
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04 AS base
|
|
RUN apt-get update && \
|
|
apt-get upgrade -y && \
|
|
apt install -y \
|
|
python3-pip \
|
|
git \
|
|
python3-venv \
|
|
wget \
|
|
gnupg \
|
|
vim \
|
|
curl
|
|
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
|
|
RUN echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list
|
|
RUN apt-get update && \
|
|
apt-get install -y \
|
|
google-chrome-stable
|
|
RUN rm -rf /var/lib/apt/lists/*
|
|
|
|
# Create directory structure
|
|
RUN mkdir -p \
|
|
/root/sia \
|
|
/root/sia/scripts \
|
|
/root/data/iterations \
|
|
/root/data/user \
|
|
/root/data/tasks \
|
|
/root/data/environment \
|
|
/root/models \
|
|
/root/desktop \
|
|
/root/venvs
|
|
|
|
# ITB tool setup
|
|
FROM base AS itb-env
|
|
COPY ./scripts/setup_binaries.py /root/sia/scripts/
|
|
COPY ./tools/itb/setup.py /root/sia/tools/itb/setup.py
|
|
RUN python3 /root/sia/scripts/setup_binaries.py /root/sia/tools/itb/setup.py
|
|
RUN python3 -m venv /root/venvs/itb
|
|
RUN /root/venvs/itb/bin/pip install -e /root/sia/tools/itb/
|
|
|
|
# Train tool setup
|
|
FROM base AS train-env
|
|
COPY ./scripts/setup_binaries.py /root/sia/scripts/
|
|
COPY ./tools/train/setup.py /root/sia/tools/train/setup.py
|
|
RUN python3 /root/sia/scripts/setup_binaries.py /root/sia/tools/train/setup.py
|
|
RUN python3 -m venv /root/venvs/train
|
|
RUN /root/venvs/train/bin/pip install -e /root/sia/tools/train/
|
|
|
|
# SIA core setup
|
|
FROM base AS sia-env
|
|
COPY ./scripts/setup_binaries.py /root/sia/scripts/
|
|
COPY ./setup.py /root/sia/setup.py
|
|
RUN python3 /root/sia/scripts/setup_binaries.py /root/sia/setup.py
|
|
RUN python3 -m venv /root/venvs/sia
|
|
RUN /root/venvs/sia/bin/pip install -e /root/sia/
|
|
|
|
# Web frontend build
|
|
FROM node:20-alpine AS web-build
|
|
WORKDIR /app
|
|
COPY web/package*.json ./
|
|
RUN npm install
|
|
COPY web .
|
|
RUN npm run build
|
|
|
|
# Final image
|
|
FROM base
|
|
|
|
# Copy virtual environments (these layers only change if setup.py files change)
|
|
COPY --from=itb-env /root/venvs/itb /root/venvs/itb
|
|
COPY --from=train-env /root/venvs/train /root/venvs/train
|
|
COPY --from=sia-env /root/venvs/sia /root/venvs/sia
|
|
|
|
# Copy source code and scripts (these change frequently but don't affect venv layers)
|
|
COPY --from=itb-env /root/sia/tools/itb /root/sia/tools/itb
|
|
COPY --from=train-env /root/sia/tools/train /root/sia/tools/train
|
|
COPY --from=sia-env /root/sia /root/sia
|
|
COPY --from=web-build /app/dist /root/static/
|
|
|
|
ENV PATH="/root/venvs/*/bin:${PATH}"
|
|
WORKDIR /root/desktop
|
|
|
|
CMD ["/root/sia/scripts/restart.sh"]
|
|
===== FILE: procedures/filesystem_design/reasoning.md =====
|
|
# Filesystem Usage Reasoning
|
|
|
|
SIA's filesystem organization is foundational to its operation and self-improvement capabilities. This document focuses specifically on how files and directories should be structured to support SIA's core functions across different environments.
|
|
|
|
The filesystem design addresses several key requirements:
|
|
|
|
- **Self-Modification**: Supporting SIA's ability to modify its own code and resources
|
|
- **Persistence**: Ensuring critical data survives across container restarts and environment changes
|
|
- **Environment Portability**: Maintaining consistent operation in local development and cloud environments
|
|
- **Sub-Instance Management**: Enabling isolated filesystem views for testing
|
|
- **Tool Accessibility**: Organizing tools to be both modifiable and accessible
|
|
|
|
These requirements often have competing demands, requiring careful balance between flexibility, consistency, and isolation.
|
|
|
|
## Runtime Filesystem Structure
|
|
|
|
### Core Directories
|
|
|
|
The SIA runtime filesystem is organized into distinct areas with different purposes and persistence characteristics:
|
|
|
|
```
|
|
/root/
|
|
├── sia/ # Git repository containing code, configuration, and documentation
|
|
├── data/ # Central location for all persistent storage
|
|
│ ├── iterations/ # Operation history recorded as XML files
|
|
│ ├── user/ # User-specific information and preferences
|
|
│ ├── tasks/ # Task tracking and management
|
|
│ ├── environment/ # Information about the runtime environment
|
|
│ └── ... # Other persistent data directories as needed
|
|
├── models/ # LLM model files, organized by version
|
|
│ ├── 1234567890abcdef/ # Specific versioned model
|
|
│ ├── abcdef1234567890/ # Another versioned model
|
|
│ └── current/ # Symlink to the currently active model
|
|
└── desktop/ # Ephemeral workspace for temporary files
|
|
├── test_environments/ # Sub-instance test environments
|
|
├── task_workspaces/ # Temporary files for current tasks
|
|
└── ... # Other ephemeral workspaces
|
|
```
|
|
|
|
### Purpose and Characteristics
|
|
|
|
Each top-level directory serves a specific purpose with distinct persistence characteristics:
|
|
|
|
- **`/root/sia/`**
|
|
- Contains the complete git repository
|
|
- All source code, configurations, and documentation
|
|
- Modified through self-improvement processes
|
|
- Synchronized with remote git repository
|
|
- Mounted from host in development, cloned in cloud environments
|
|
|
|
- **`/root/data/`**
|
|
- Central location for all persistent data
|
|
- Mounted as persistent storage in all environments
|
|
- Critical for operational continuity
|
|
- Requires regular backup and synchronization
|
|
- Contains subdirectories for different types of persistent data:
|
|
- `iterations/`: Records of SIA's reasoning and actions
|
|
- `user/`: Information about users SIA interacts with
|
|
- `tasks/`: Current and completed task information
|
|
- `environment/`: Information about the current environment
|
|
|
|
- **`/root/models/`**
|
|
- Storage for LLM model weights
|
|
- Organized by git commit id of the config file it was trained on
|
|
- `current/` symlink points to active model
|
|
- Enables easy switching between model versions
|
|
- Mounted for local development
|
|
- Ephemeral since models can be trained on the fly
|
|
|
|
- **`/root/desktop/`**
|
|
- Ephemeral workspace for temporary operations
|
|
- Can be lost without compromising system integrity
|
|
- Used for testing environments, task workspaces, and intermediate files
|
|
- Cleared periodically to prevent accumulation of stale data
|
|
|
|
This organization creates clear boundaries between:
|
|
- Code (version controlled)
|
|
- Persistent data (requires preservation)
|
|
- Model files (versioned artifacts)
|
|
- Temporary workspaces (disposable)
|
|
|
|
## Persistent Data Management
|
|
|
|
### Data Categories
|
|
|
|
The `/root/data/` directory contains several categories of persistent information:
|
|
|
|
- **Iterations** (`/root/data/iterations/`)
|
|
- Complete history of SIA's operation
|
|
- XML files containing context and responses
|
|
- Organized chronologically
|
|
- Used for training data extraction and operational history
|
|
|
|
- **User Information** (`/root/data/user/`)
|
|
- User preferences and characteristics
|
|
- Conversation history
|
|
- User-specific settings
|
|
- Critical for continuity in user interactions
|
|
|
|
- **Task State** (`/root/data/tasks/`)
|
|
- Current task descriptions and status
|
|
- Task history and outcomes
|
|
- Links to relevant files and resources
|
|
- Essential for resuming work after interruptions
|
|
|
|
- **Environment Information** (`/root/data/environment/`)
|
|
- Information about the current runtime environment
|
|
- URL for the SIA git repository
|
|
- Kind of deployment (e.g., local, cloud, sub-instance)
|
|
|
|
Additional persistent data categories may be added as SIA's capabilities evolve.
|
|
|
|
### Persistence Strategy
|
|
|
|
Different environments require different approaches to ensure data persistence:
|
|
|
|
- **Local Development**
|
|
- `/root/data/` directory is mounted as a Docker volume
|
|
- Maps to a local directory on the development machine
|
|
- Persists across container restarts automatically
|
|
- Easy to back up through standard filesystem operations
|
|
|
|
- **Cloud Deployment (e.g., RunPod)**
|
|
- `/root/data/` mapped to a persistent volume
|
|
- Cloud provider ensures data survives instance restarts
|
|
|
|
- **Sub-Instances**
|
|
- Each sub-instance receives a properly initialized `/root/data/` directory
|
|
- Contains minimal necessary data for the specific test case
|
|
- Can be discarded after test completion
|
|
- Important results extracted and preserved before cleanup
|
|
|
|
## Model Management
|
|
|
|
### Model Organization
|
|
|
|
LLM models are stored in `/root/models/` with a clear version-based structure:
|
|
|
|
- **Versioned Directories**
|
|
- Models stored in separate directories named by the commit id of the training config file
|
|
- Contains all files needed for the model (weights, tokenizer config, etc.)
|
|
|
|
- **Current Model Symlink**
|
|
- `/root/models/current/` is a symbolic link to the active model
|
|
- Allows code to reference a consistent path regardless of which model is active
|
|
- Switching models is accomplished by updating this symlink
|
|
|
|
### Model Persistence
|
|
|
|
Models require special handling due to their size:
|
|
|
|
- **Local Development**
|
|
Models are stored in a local directory mounted to `/root/models/`.
|
|
This directory is in the .gitignore.
|
|
|
|
- **Cloud Deployment**
|
|
The initial model is downloaded and finetuned by the bootstrap script.
|
|
|
|
## Git Repository Management
|
|
|
|
### Repository Organization
|
|
|
|
The SIA git repository at `/root/sia/` contains all code and configuration:
|
|
|
|
- **Code Organization**
|
|
- Core SIA package in `/root/sia/sia/`
|
|
- Procedures in `/root/sia/procedures/`
|
|
- Tools in `/root/sia/tools/`
|
|
- Documentation in appropriate locations throughout the repository
|
|
|
|
- **Version Control**
|
|
- All code modifications tracked through git
|
|
- Branching strategy for self-improvement defined in the git workflow procedure
|
|
- Commit history provides audit trail of system evolution
|
|
|
|
### Repository Access
|
|
|
|
Access to the git repository varies by environment:
|
|
|
|
- **Local Development**
|
|
- Repository directory mounted directly from host
|
|
- Changes immediately visible to the developer
|
|
- No need to push changes to remote repository
|
|
|
|
- **Cloud Deployment**
|
|
- Repository cloned during bootstrap
|
|
- Dedicated git credentials for SIA self-modification
|
|
- Changes pushed to remote repository
|
|
|
|
## Sub-Instance Filesystem Management
|
|
|
|
### Sub-Instance Requirements
|
|
|
|
Sub-instances need isolated filesystem views for testing and development:
|
|
|
|
- **Code Isolation**
|
|
- Access to specific versions of SIA code
|
|
- Potentially modified for testing purposes
|
|
- Protected from changes by other instances
|
|
|
|
- **Data Initialization**
|
|
- Properly initialized `/root/data/` with necessary state
|
|
- Test-specific user and task information
|
|
- Clean iterations directory for recording test behavior
|
|
|
|
- **Model Access**
|
|
- Access to appropriate model files
|
|
- Potentially different from the parent instance's model
|
|
|
|
### Sub-Instance Structure
|
|
|
|
Each sub-instance gets a controlled environment in `/root/desktop/test_environments/`:
|
|
|
|
```
|
|
/root/desktop/test_environments/
|
|
├── instance_123/ # Specific test instance
|
|
│ ├── sia/ # Copy or modified version of SIA code
|
|
│ ├── data/ # Initialized data directory
|
|
│ │ ├── iterations/ # Empty or seeded with initial state
|
|
│ │ ├── user/ # Test-specific user data
|
|
│ │ ├── tasks/ # Test-specific tasks
|
|
│ │ └── environment/ # Test-specific environment data
|
|
│ └── models/ # Access to necessary models
|
|
│ └── current/ # Symlink to appropriate model version
|
|
└── ... # Other test instances
|
|
```
|
|
|
|
The managing instance maintains responsibility for:
|
|
- Creating these isolated environments
|
|
- Monitoring the sub-instance's operation
|
|
- Collecting test results
|
|
- Cleaning up after tests complete
|
|
|
|
## Bootstrap Process
|
|
|
|
When starting in a new environment, SIA requires initialization:
|
|
|
|
1. **Repository Preparation**
|
|
- Check for existence of repository at `/root/sia/`
|
|
- Clone the repository at `/root/sia/`
|
|
2. **Model Setup**
|
|
- Check for existence of current model
|
|
- Start finetune job for HEAD commit
|
|
- Create/update the `/root/models/current/` symlink
|
|
|
|
3. **Data Initialization**
|
|
- Initialize `/root/data/` with minimal required structure
|
|
- Restore data from backups when available (future work)
|
|
===== FILE: procedures/README.md =====
|
|
# Procedures
|
|
|
|
Procedures are step-by-step instructions that AI agents can follow to complete complex tasks.
|
|
They create a framework for solving problems in a consistent manner while enabling continuous improvement through analysis and adaptation.
|
|
Procedures are a guideline, if they don't work the agent can adapt to the situation.
|
|
|
|
## Core Concepts
|
|
|
|
A procedure is a flowchart-style guide that an agent can follow to complete a task.
|
|
Each procedure is stored in its own directory and contains files that separate different concerns:
|
|
|
|
- **Discovery**: Quick identification of relevant procedures
|
|
- **Execution**: Clear steps and dependencies
|
|
- **Analysis**: Understanding of effectiveness and issues
|
|
- **Evolution**: Natural path to optimization and training
|
|
|
|
## Directory Structure
|
|
|
|
```
|
|
procedures/
|
|
└── example_procedure/
|
|
├── description.md # Quick discovery info
|
|
├── procedure.md # Main procedure flow
|
|
├── reasoning.md # Analysis and rationale
|
|
├── history/ # Usage records
|
|
│ └── 20250106_131420_405/
|
|
│ ├── iteration_20250106_131423_051.xml
|
|
│ └── iteration_20250106_131424_226.xml
|
|
└── training/ # Optional training data
|
|
├── short_description_of_goal
|
|
│ ├── iteration_20250106_131423_051.xml
|
|
│ └── iteration_20250106_131424_226.xml
|
|
└── another_goal
|
|
└── ...
|
|
```
|
|
|
|
### File Purposes
|
|
|
|
#### description.md
|
|
Contains a short description and keywords that help agents find relevant procedures. This file should be quick to read and clearly indicate the procedure's purpose and applicability.
|
|
|
|
```markdown
|
|
Send an email using the users email account.
|
|
|
|
- communication
|
|
- gmail
|
|
```
|
|
|
|
#### procedure.md
|
|
The main file loaded when executing the procedure. Contains:
|
|
- Mermaid diagram showing the procedure flow
|
|
- Referenced procedures
|
|
- Prerequisites for execution
|
|
|
|
````markdown
|
|
# Sending an Email
|
|
|
|
## Referenced Procedures
|
|
- web usage (ITB)
|
|
- managing user information
|
|
|
|
## Prerequisites
|
|
- If the email requires attachements, these should be available as files in the SIA filesystem
|
|
|
|
## Flow
|
|
```mermaid
|
|
...
|
|
```
|
|
````
|
|
|
|
#### reasoning.md
|
|
Documents why the procedure works the way it does. Contains:
|
|
- Design rationale
|
|
- History of uses and outcomes
|
|
- Notes about successful and failed executions
|
|
- Analysis of issues and improvements
|
|
|
|
New analysis is appended after each use of the procedure.
|
|
|
|
#### history/
|
|
Contains timestamped directories for each use of the procedure.
|
|
The timestamp comes from the ID of the script that loads the procedure.md file.
|
|
|
|
## Usage Flow
|
|
|
|
### Discovery
|
|
When an agent needs to complete a task:
|
|
1. Search through description.md files
|
|
2. Match keywords to task requirements
|
|
3. Identify relevant procedures
|
|
|
|
### Execution
|
|
Once a procedure is selected:
|
|
1. Load procedure.md for execution
|
|
2. Follow the flowchart
|
|
3. Copy iterations to new history subdirectory
|
|
4. Analyze execution success and issues
|
|
5. Append findings to reasoning.md
|
|
|
|
### Optimization
|
|
During idle time:
|
|
1. Review procedure usage patterns
|
|
2. Identify improvement opportunities
|
|
3. Generate training data if appropriate
|
|
4. Update procedure flow if needed
|
|
===== FILE: procedures/self_improvement/reasoning.md =====
|
|
# Self Improvement
|
|
|
|
SIA operates as a learning system that constantly seeks to enhance its capabilities while maintaining stable and reliable operation.
|
|
This improvement happens through several complementary mechanisms, each serving different aspects of the system's evolution.
|
|
|
|
## Improvement Mechanisms
|
|
|
|
The system can improve in several ways:
|
|
|
|
### LLM Finetuning
|
|
|
|
The foundation of SIA's intelligence is its Large Language Model.
|
|
Through careful analysis of its performance, SIA identifies cases where its reasoning or actions could have been better.
|
|
These examples, when properly validated and organized, become valuable training data for improving the model's capabilities.
|
|
|
|
### Source Code Evolution
|
|
|
|
SIA can modify its own Python source code, enabling both architectural and functional improvements.
|
|
When stopped, SIA restarts and reloads the Python files, allowing code changes to take effect.
|
|
While powerful, this capability requires robust validation to prevent system instability.
|
|
|
|
### Procedure Refinement
|
|
|
|
Procedures provide guided reasoning paths for complex tasks.
|
|
By analyzing its operational history, SIA can identify common patterns and create new procedures or optimize existing ones.
|
|
This improves consistency and efficiency while carrying lower risk than direct code modifications.
|
|
|
|
### Tool Development
|
|
|
|
SIA can develop new tools and enhance existing ones, expanding its capabilities by creating new ways to interact with systems and data.
|
|
|
|
## Challenge-Based Testing
|
|
|
|
To improve systematically, SIA uses challenges that formalize specific capabilities into testable scenarios.
|
|
|
|
These serve multiple purposes:
|
|
|
|
- Testing specific capabilities
|
|
- Validating improvements
|
|
- Detecting regressions
|
|
- Guiding learning
|
|
- Documenting known limitations
|
|
|
|
### Challenge Structure
|
|
|
|
Each challenge consists of:
|
|
|
|
1. Description
|
|
- Clear statement of what capability is being tested
|
|
- Why this capability matters
|
|
- Success criteria
|
|
- Resource constraints
|
|
|
|
2. Starting State
|
|
- Filesystem state
|
|
- Starting system context
|
|
- Scripts for creating files
|
|
- User info or documentation on how and what to communicate
|
|
|
|
3. Validation
|
|
- Precise success criteria
|
|
- Resource limits (CPU, memory, time)
|
|
- Required outputs or state changes
|
|
- What to measure or track
|
|
|
|
### Example Challenges
|
|
|
|
Two examples illustrate different aspects of capabilities that need testing.
|
|
Together they are good examples because they:
|
|
|
|
- Test different capability sets
|
|
- Have clear success criteria
|
|
- Include resource constraints
|
|
- Require multiple skills
|
|
- Reflect real-world tasks
|
|
|
|
#### Time Series Analysis Challenge
|
|
|
|
Tests if SIA can effectively analyze large amounts of time series data spread across multiple files. This tests:
|
|
|
|
- Efficient file handling
|
|
- Context management with size constraints
|
|
- Data analysis capabilities
|
|
- Resource-aware processing
|
|
|
|
##### Initial setup
|
|
|
|
- Directory with many CSV files containing time series data (created by a script)
|
|
- Files too large to load into context at once
|
|
- Need to find patterns/anomalies in the data
|
|
- Resource constraints on memory usage
|
|
|
|
##### Success criteria
|
|
|
|
- Efficiently reads and processes files
|
|
- Manages context to stay within limits
|
|
- Uses appropriate aggregation strategies
|
|
- Finds correct answers to queries
|
|
- Validates findings across files
|
|
|
|
#### Tool Development Challenge
|
|
|
|
Tests if SIA can develop a score tracking tool based on vague user requirements. This tests:
|
|
|
|
- Requirements gathering through questions
|
|
- Software design capabilities
|
|
- User interface development
|
|
- Integration with existing systems
|
|
- Documentation skills
|
|
|
|
##### Initial setup
|
|
|
|
- Simple user request for card game score tracking
|
|
- Ability to ask questions about requirements
|
|
|
|
##### Success criteria
|
|
|
|
- Asks relevant questions to understand needs
|
|
- Designs appropriate solution
|
|
- Implements working tool
|
|
- Documents usage clearly
|
|
|
|
#### Example Directory Structure
|
|
|
|
Challenges are organized in a consistent file structure:
|
|
|
|
```
|
|
challenges/
|
|
├── timeseries_analysis/
|
|
│ ├── description.md # Core challenge description and how to prepare the environment
|
|
│ ├── initial_context/
|
|
│ │ ├── context.xml # Initial context with SIA in conversation with the user
|
|
│ │ ├── user_request.txt # "Find unusual patterns in our sensor data"
|
|
│ │ ├── setup.sh # Runs data_generator.py and prepares test environment
|
|
│ │ └── data_generator.py # Script to generate test data
|
|
│ │
|
|
│ └── validation/
|
|
│ ├── criteria.md # Expected findings, memory limits, performance targets
|
|
│ ├── presentation.txt # Info on how the results should be formatted for a perfect score
|
|
│ └── known_patterns.md # Lists patterns inserted by generator for validation
|
|
|
|
|
└── score_tracker/
|
|
├── description.md # Core challenge: create card game score tracker
|
|
├── initial_context/
|
|
│ ├── context.xml # Initial context with SIA in conversation with the user
|
|
│ ├── user_request.txt # "Need tool to track card game scores"
|
|
| └── answers/ # Valid responses to expected questions
|
|
| ├── deployment.md # "Web interface preferred, deployed on internal server"
|
|
| ├── data_storage.md # "Store in SQLite, one file per game session"
|
|
| ├── user_interaction.md # "Need to add/edit scores, see history, multiple games"
|
|
| └── conventions.md # Coding standards, project structure requirements
|
|
│
|
|
└── validation/
|
|
├── criteria.md # Tool requirements, user experience goals
|
|
└── test_scenarios.md # Usage scenarios tool should handle
|
|
```
|
|
|
|
This structure ensures that:
|
|
- Challenges are self-contained
|
|
- Test setup is reproducible
|
|
- Success criteria are clear
|
|
- Required resources are documented
|
|
- SIA can get clarification when needed
|
|
|
|
### Results and metrics
|
|
|
|
In the criteria.md file, each criterion is defined as a level-1 header with several key pieces of information:
|
|
- A clear description of what is being measured
|
|
- The type and unit of the measurement
|
|
- The valid and expected range of values
|
|
- A pass/fail thresholds
|
|
- Details about how the measurement should be performed
|
|
|
|
From the criteria file and the iterations dir, a test report can be generated.
|
|
This test report includes:
|
|
- The challenge name
|
|
- The commit id of the SIA repo when the test was run
|
|
- Timestamp of the first iteration
|
|
- The overall pass/fail status
|
|
- Context usage (average and maximum)
|
|
- All metrics as described in the criteria.md file
|
|
|
|
#### Example
|
|
|
|
criteria.md file for the "Time Series Analysis Challenge"
|
|
|
|
```markdown
|
|
...
|
|
# Presentation
|
|
|
|
This metric indicates how clear the results are presented to the user.
|
|
It looks at communication and formating of the report.
|
|
|
|
The score starts with 0.
|
|
If the discontinuity is found and the agent reports the index where it starts, the score is increased by 1.
|
|
If the agent adds context info, e.g. a graph, a table, or explanation on how the result was obtained, the score is 2.
|
|
If the agent requests how the data should be presented and follows instructions, the score is 3.
|
|
|
|
The challenge fails if the formatting score is 0.
|
|
|
|
Presentation instructions for the agent can be found in presentation.txt.
|
|
...
|
|
```
|
|
|
|
report.json file for the "Time Series Analysis Challenge"
|
|
|
|
```json
|
|
{
|
|
"challenge": "Time Series Analysis",
|
|
"commit": "1234567890abcdef",
|
|
"timestamp": "2023-05-01T12:00:00Z",
|
|
"status": "pass",
|
|
"context_max": 74,
|
|
"context_avg": 56,
|
|
...
|
|
"presentation": 2,
|
|
...
|
|
}
|
|
```
|
|
|
|
## Testing Framework
|
|
|
|
Testing self-improvement capabilities requires running SIA instances in various scenarios while monitoring their behavior and performance.
|
|
These test instances must operate in isolation to prevent interference with the main instance or each other.
|
|
However, they must also be observable so that the managing instance can evaluate their performance and collect data for improvement.
|
|
|
|
The framework needs to handle several key tasks:
|
|
|
|
- Creating isolated test environments with specific initial conditions
|
|
- Starting and managing test SIA instances
|
|
- Monitoring test instance behavior and performance
|
|
- Collecting and analyzing results
|
|
- Cleaning up test environments
|
|
|
|
### Process Isolation with Bubblewrap
|
|
|
|
For running test instances, we chose Bubblewrap (bwrap) over alternatives like Docker-in-Docker or systemd-nspawn.
|
|
This decision was driven by several factors:
|
|
|
|
Bubblewrap provides excellent process isolation while being significantly lighter than full container solutions.
|
|
It allows fine-grained control over the filesystem view, process namespaces, and security boundaries.
|
|
Unlike Docker-in-Docker, it doesn't require privileged containers or risk storage driver conflicts.
|
|
Compared to systemd-nspawn, it offers more granular control and doesn't require root privileges or systemd integration.
|
|
|
|
The tool's ability to create custom filesystem views is particularly valuable for testing.
|
|
We can construct exactly the environment needed for each test, including mock files and services, without maintaining full container images.
|
|
This makes it easy to create and dispose of test environments quickly.
|
|
|
|
Bubblewrap's user-space operation means test instances can be started without special privileges, making the testing framework more secure and easier to use in various environments.
|
|
Its lightweight nature also means we can run many test instances efficiently, which is important for parallel testing and rapid iteration.
|
|
|
|
### Communication and Monitoring
|
|
|
|
Test instances communicate with the managing instance primarily through standard input and output streams.
|
|
This choice leverages SIA's existing I/O handling capabilities without requiring special modifications for testing.
|
|
The managing instance can send inputs through stdin and observe responses through stdout, just like a normal user would.
|
|
|
|
For deeper inspection, the managing instance monitors the test instance's iterations directory.
|
|
Since SIA already serializes its context for each iteration, this provides a complete record of the test instance's behavior without requiring additional instrumentation.
|
|
This approach maintains clean separation between the test instance and the monitoring system.
|
|
|
|
### Test Results Storage
|
|
|
|
Test results are organized hierarchically:
|
|
|
|
```
|
|
test_runs/
|
|
20240120_131415_commit_abc123/ # Timestamp and commit
|
|
challenge_name/
|
|
iterations/ # Complete iteration history
|
|
io.log # External interaction log
|
|
report.json # Test report
|
|
```
|
|
|
|
The io.log captures all external interactions:
|
|
|
|
```
|
|
2024-01-20T13:14:15.123Z stdin Can you send an email to sarah@example.com about the project status?
|
|
2024-01-20T13:14:15.892Z stdout I'll help you draft an email about the project status. Based on recent project information, I suggest this message:
|
|
|
|
Subject: Project Status Update
|
|
Content:
|
|
Hi Sarah,
|
|
|
|
I wanted to provide a quick update on our project progress. We've completed the initial phase of development and are on track with our timeline. The team has addressed all critical issues from the last review.
|
|
|
|
Would you like me to send this email?
|
|
2024-01-20T13:14:20.456Z stdin Yes, that looks good. Please send it.
|
|
2024-01-20T13:14:20.789Z stdout I'll send the email now.
|
|
2024-01-20T13:14:21.012Z mailbox:sarah@example.com Subject: Project Status Update
|
|
Hi Sarah,
|
|
|
|
I wanted to provide a quick update on our project progress. We've completed the initial phase of development and are on track with our timeline. The team has addressed all critical issues from the last review.
|
|
2024-01-20T13:14:21.234Z stdout Email has been sent to sarah@example.com successfully.
|
|
```
|
|
|
|
## Test Environment Setup
|
|
|
|
Test environments are created using a dedicated setup tool.
|
|
This tool manages the directory structure, initial context, and any required background processes.
|
|
|
|
The initial context for a test instance can include setup actions that are executed when the instance starts.
|
|
Instead of trying to recreate a specific state directly, we let SIA execute these actions naturally.
|
|
This approach handles both the creation of background processes and the establishment of initial conditions in a way that maintains proper timing relationships.
|
|
|
|
Each entry in the initial context uses relative timestamps (offsets from the start time).
|
|
When SIA executes these entries, they automatically align with the new instance's timeline.
|
|
This preserves the temporal relationships between entries while anchoring them to the test instance's actual start time.
|
|
|
|
## Training Configuration
|
|
|
|
SIA takes a modular approach to model training by having separate specialized tools for each provider like train_mistral, train_deepseek, etc.
|
|
Each tool shares similar core functionality while handling provider-specific requirements.
|
|
The default training tool and parameters are called from the `/root/sia/tools/train/train.sh` script.
|
|
|
|
While the training process is conceptually similar across providers, each has unique requirements for data formatting, API interactions, and job management.
|
|
By creating dedicated tools, we can properly encapsulate these differences without complicating the core training logic.
|
|
For example, Mistral needs JSONL files with specific message structures, while other providers might require different formats or metadata.
|
|
|
|
Training configuration should be consistent regardless of the provider.
|
|
All training tools read from the same config.yaml format, which defines essential parameters like the system prompt, action schema, and training data paths.
|
|
These parameters represent fundamental aspects of how we want the model to behave, independent of which provider handles the actual training.
|
|
The tools then translate these standard parameters into provider-specific settings.
|
|
|
|
Training tools enforce important safeguards around version control.
|
|
Before starting a training run, each tool verifies that all source files - including the config itself, training data, system prompt, and action schema - are committed to git.
|
|
This ensures reproducibility by guaranteeing we can recreate the exact training conditions that produced any given model.
|
|
The git commit hash becomes part of the internal tracking of model versions.
|
|
|
|
The tools follow a common workflow:
|
|
1. Read and validate the standard config.yaml format
|
|
2. Check that all source files are committed to git
|
|
3. Convert training data into the provider's required format
|
|
4. Upload data through the provider's API
|
|
5. Start training with the specified parameters
|
|
6. Return job information for monitoring progress
|
|
|
|
This separation of concerns makes it easier to:
|
|
- Add support for new training providers without changing existing code
|
|
- Maintain consistent training configuration across different providers
|
|
- Track and reproduce training runs reliably
|
|
- Handle provider-specific error cases and requirements appropriately
|
|
- Update individual providers' implementations as their APIs evolve
|
|
|
|
## Repository Structure
|
|
|
|
All components that define SIA's behavior are version controlled in a single repository, providing a clear and reproducible state for any point in time.
|
|
|
|
This is used in two ways:
|
|
|
|
1. **Training**
|
|
|
|
The training configuration defines parameters and data used for training.
|
|
A model, retrained from the same commit, should produce the same results.
|
|
Trained models reference the commit id.
|
|
|
|
2. **Testing**
|
|
|
|
Test reports contain the commit id to track changes in the challenges.
|
|
|
|
The repository contains:
|
|
- Source code for SIA and its tools
|
|
- Procedures for handling various tasks
|
|
- Training configuration and data
|
|
- Test results and analysis
|
|
|
|
## Continuous Operation
|
|
|
|
While working on improvements, SIA must maintain its core functionality:
|
|
- Monitoring for user input
|
|
- Managing running tasks
|
|
- Handling scheduled events
|
|
- Tracking system resources
|
|
|
|
This requires:
|
|
1. Active Monitoring
|
|
- Check for user input on stdin
|
|
- Track running background tasks
|
|
- Handle scheduled events
|
|
- Monitor system resources
|
|
|
|
2. Interruption Handling
|
|
- Pause improvement work when needed
|
|
- Store progress and state
|
|
- Clean up temporary resources
|
|
- Maintain context awareness
|
|
|
|
3. Resource Management
|
|
- Balance improvement with responsiveness
|
|
- Monitor system resource usage
|
|
- Prevent training impact on user tasks
|
|
- Clean up old data regularly
|
|
===== FILE: procedures/tool_management/reasoning.md =====
|
|
# Tool Management
|
|
|
|
SIA uses and creates tools to extend its capabilities.
|
|
A tool in the SIA context refers to a clearly packaged software component that provides specific functionality.
|
|
This document outlines how tools are structured, managed, and developed.
|
|
All tools reside in the `/root/sia/tools/` directory and are part of the main git repository.
|
|
|
|
## Typical patterns
|
|
|
|
Tools in SIA follow these established patterns:
|
|
|
|
- **Independence**: Each tool functions as a standalone component with clear boundaries
|
|
- **Python**: The preferred language for tools is Python
|
|
- **Installation**: The Dockerfile and bootstrap script intall all tools in the directory using `pip install -e`
|
|
- **Documentation**: Each tool has a README.md file that provides requirements, usage instructions and documentation
|
|
- **Modification**: Tools can be modified by SIA as part of its self-improvement process
|
|
- **Procedures**: Tools are created to support procedures
|
|
- **Training**: No separate training data is maintained, this is included in the procedure training
|
|
|
|
## Tool Directory Structure
|
|
|
|
Each tool follows a consistent structure:
|
|
|
|
```
|
|
tools/
|
|
└── tool_name/
|
|
├── README.md # Documentation
|
|
├── requirements.txt # Tool-specific dependencies
|
|
├── setup.py # Installation configuration
|
|
├── tool_name/ # Main package code
|
|
│ └── __init__.py
|
|
├── bin/ # Command-line executables
|
|
│ └── tool_command
|
|
└── test/ # Tool-specific tests
|
|
└── test_tool.py
|
|
```
|
|
===== FILE: procedures/user_communication/description.md =====
|
|
Manage communication with users through standard input/output.
|
|
Handle both direct responses and conversation flows, with appropriate context management and response timing.
|
|
|
|
- user interaction
|
|
- conversation
|
|
- messages
|
|
- response handling
|
|
===== FILE: procedures/user_communication/procedure.md =====
|
|
# User Communication
|
|
|
|
## Prerequisites
|
|
- User information is stored in the /user directory
|
|
- Tasks and their progress are documented in the /tasks directory
|
|
|
|
## Flow
|
|
```mermaid
|
|
flowchart TD
|
|
Start([The /root/data/user/basic.md file contains info to identify and address the user<br>Load the last 10 messages from /root/data/user/conversation_history/ ])
|
|
PrepareForDraft{Have everything needed for drafting a message?}
|
|
DraftMessage[Draft message in reasoning entry]
|
|
ReadInput[Read input from standard input]
|
|
AddHistoryUser[Add the message to the /root/data/user/conversation_history/ directory<br>The filename is the id of the stdin entry with .user extension]
|
|
LoadTask[Look for the task in the /root/data/tasks directory and load relevant files]
|
|
LoadUserDetails[Look in the /root/data/user directory for relevant files]
|
|
EstimateScript[Draft the script in a reasoning block and estimate its runtime and output length]
|
|
ScriptAcceptable{Does the draft script make sense and are the estimations short enough to not hinder the conversation?}
|
|
RunScript[Run the script, make sure to set appropriate timeout and output limits]
|
|
ReviewDraft{Is the message well structured and free of logical errors?}
|
|
SendMessage[Send the message using standard output]
|
|
AddHistoryAgent[Add the message to the /root/data/user/conversation_history/ directory<br>The filename is the id of the stdout entry with .agent extension]
|
|
ReasonResponse[Is the conversation ongoing?<br>How long is the user expected to take to respond?]
|
|
NeedAwaitResponse{Is it likely to get a response within a minute of sending the message?}
|
|
BusyWait[Wait 5 seconds<br>Make sure to set the timout]
|
|
AnalyzeContext[Create reasoning entry<br>For each entry in context indicate:<br>- Can delete<br>- Contains info to save<br>- Must keep in context]
|
|
MakeNotes[Create or update files with info to save]
|
|
End([Clean the context])
|
|
|
|
Start --> PrepareForDraft
|
|
|
|
PrepareForDraft -->|Got all needed info| DraftMessage
|
|
PrepareForDraft -->|Getting the required info would slow the conversation| DraftMessage
|
|
PrepareForDraft -->|Input available on stdin| ReadInput
|
|
PrepareForDraft -->|Task mentioned but not loaded| LoadTask
|
|
PrepareForDraft -->|Personal or social info mentioned but not loaded| LoadUserDetails
|
|
PrepareForDraft -->|Calculations, system info or other numerical values that can be scripted are mentioned| EstimateScript
|
|
|
|
ReadInput --> AddHistoryUser
|
|
AddHistoryUser --> PrepareForDraft
|
|
|
|
LoadTask --> PrepareForDraft
|
|
LoadUserDetails --> PrepareForDraft
|
|
|
|
EstimateScript --> ScriptAcceptable
|
|
ScriptAcceptable -->|Acceptable| RunScript
|
|
ScriptAcceptable -->|Not acceptable| PrepareForDraft
|
|
RunScript --> PrepareForDraft
|
|
|
|
DraftMessage --> ReviewDraft{Is this really what I want to say?}
|
|
ReviewDraft -->|Rewrite better| DraftMessage
|
|
ReviewDraft -->|Good message| SendMessage
|
|
|
|
SendMessage --> AddHistoryAgent
|
|
AddHistoryAgent --> ReasonResponse
|
|
ReasonResponse --> NeedAwaitResponse
|
|
|
|
NeedAwaitResponse -->|Quick response is unlikely| AnalyzeContext
|
|
NeedAwaitResponse -->|Input available on stdin| PrepareForDraft
|
|
NeedAwaitResponse -->|Quick response is likely| BusyWait
|
|
BusyWait --> NeedAwaitResponse
|
|
|
|
AnalyzeContext --> MakeNotes
|
|
MakeNotes --> End
|
|
```
|
|
===== FILE: procedures/user_communication/reasoning.md =====
|
|
# Design Rationale
|
|
|
|
## Core Structure
|
|
The procedure is designed to maintain a clean conversation flow while ensuring all necessary information is available.
|
|
The flow focuses on:
|
|
|
|
1. **Context management**
|
|
- Load basic user info immediately to ensure proper interaction
|
|
- Recent conversation history provides continuity
|
|
- Clean context when usage exceeds 50%
|
|
- Gather only what's needed for the current conversation
|
|
- Skip gathering if it would slow down the interaction
|
|
|
|
2. **Message Management**
|
|
- Draft-review-revise cycle for quality
|
|
- Automatic history tracking for both user and agent messages
|
|
- Clear file naming convention using entry IDs
|
|
|
|
3. **Script Handling**
|
|
- Pre-execution estimation of runtime and output
|
|
- Explicit acceptability check before running
|
|
- Timeout and output limits for safety
|
|
|
|
4. **Response Timing**
|
|
- Adaptive busy wait with exponential backoff
|
|
- Clear decision point for wait vs end
|
|
- Proper context cleanup on exit
|
|
|
|
## Usage History
|
|
|
|
===== FILE: procedures/using_procedures/procedure.md =====
|
|
# Using Procedures
|
|
|
|
## Core Guidelines
|
|
|
|
When following a procedure's flowchart:
|
|
|
|
1. Start with reasoning:
|
|
- State current position in flowchart explicitly
|
|
- Focus on immediate next step, don't go ahead of the chart
|
|
- Evaluate current context and task state
|
|
- Often state entry id's that can be removed and explain why to avoid mistakes
|
|
- State expected output and runtime for scripts
|
|
|
|
2. After script execution:
|
|
- Analyze the actual output and compare with the expected output
|
|
- Reevaluate situation based on results
|
|
- Return to flowchart for next step
|
|
- Consider if current path is still appropriate
|
|
|
|
3. When a procedure fails:
|
|
- Create a task explaining the issue and the need to fix it
|
|
- Add timestamps and id's of relevant entries
|
|
|
|
Note: Procedures will mention file names to use.
|
|
If these files don't exists it may be necessary to create them or to continue without.
|
|
Only create files when you are sure of the info that goes inside.
|
|
In later iterations you will consider this info as fact.
|
|
Halucinations and guesses are detrimental to the correct functioning of the agent.
|
|
|
|
## Attention Management
|
|
|
|
LLMs pay more attention to recently mentioned information.
|
|
Reasoning entries should mention what needs attention now.
|
|
|
|
To maintain focus:
|
|
1. State current flowchart position in each reasoning
|
|
2. Quote relevant parts of important entries
|
|
3. Reference specific entry IDs when using their information
|
|
4. Periodically remind about ongoing tasks or future needs
|
|
5. Clean up entries that aren't needed for current step
|
|
|
|
Example of good attention management:
|
|
```xml
|
|
<reasoning>
|
|
At node "evaluate_test_results". Entry 45f3d2 shows failed test: "Error: Connection timeout".
|
|
Will need to check system logs soon (noted in /root/data/tasks/reminders.txt, check at 14:00).
|
|
First focusing on this error.
|
|
</reasoning>
|
|
```
|
|
|
|
## Reasons to Switch Procedures
|
|
|
|
Common triggers:
|
|
- Data available on stdin
|
|
- Time matching scheduled task
|
|
- Error conditions in script output
|
|
- Resource constraints detected
|
|
- User input needed
|
|
===== FILE: procedures/using_procedures/reasoning.md =====
|
|
# Design Rationale
|
|
|
|
## Why Procedures
|
|
|
|
Procedures provide guided reasoning paths for complex tasks. The LLM engine can make mistakes when:
|
|
- Jumping to action before proper analysis
|
|
- Losing track of task context after interruptions
|
|
- Missing key steps in complex processes
|
|
- Forgetting to preserve state before transitions
|
|
|
|
Procedures help prevent these issues by:
|
|
- Encouraging reasoning before action through flowchart structure
|
|
- Providing clear decision points for state evaluation
|
|
- Identifying when sub-procedures may help
|
|
- Guiding consistent approaches to common tasks
|
|
|
|
## Task State vs Procedure Guidance
|
|
|
|
Procedures guide reasoning about tasks but don't manage task state directly. For example:
|
|
|
|
During code development:
|
|
- Task state in /tasks/: code files, test results, requirements
|
|
- Procedure guidance: when to write tests, when to debug, when to refactor
|
|
- State preserved in files before switching focus
|
|
- Context regenerated from files when returning
|
|
|
|
This separation allows:
|
|
- Clean task interruption and resumption
|
|
- Natural procedure transitions
|
|
- State preservation without rigid control
|
|
- Flexible adaptation to situations
|
|
|
|
## Common Mistakes
|
|
|
|
### Premature Context Cleaning
|
|
|
|
```xml
|
|
<context ...>
|
|
<single ... id="123">
|
|
cat /procedures/test/procedure.md
|
|
<stdout>
|
|
...
|
|
</stdout>
|
|
<stderr/>
|
|
</single>
|
|
...
|
|
<reasoning>Test failed. Moving to debug procedure.</reasoning>
|
|
</context>
|
|
|
|
<delete id="123"/>
|
|
```
|
|
|
|
This removes the active procedure entry before finishing its steps
|
|
|
|
### Keeping irrelevant info in context
|
|
|
|
LLM's have difficulty spotting duplicates and sections with a low attention score.
|
|
Procedures with explicit instructions for finding these sections can help increase their attention.
|
|
===== FILE: procedures/version_control/reasoning.md =====
|
|
# Version Control
|
|
|
|
Version control is essential for SIA's self-improvement capabilities.
|
|
It provides a way to track changes, revert to previous states when necessary, and maintain a coherent evolution of the codebase.
|
|
This document outlines the git workflow designed specifically for SIA's unique development model, where the agent itself is the primary developer.
|
|
|
|
## Core Principles
|
|
|
|
The git workflow for SIA is designed around several key principles:
|
|
|
|
- **Simplicity**: Since SIA is the sole developer, complex branching strategies designed for human teams are unnecessary
|
|
- **Traceability**: Every change should be traceable to its purpose and the reasoning behind it
|
|
- **Stability**: The master branch should always be in a working state
|
|
- **Recoverability**: It should always be possible to revert to a known good state
|
|
- **Security**: Git credentials must be handled securely to protect repository access
|
|
|
|
## Branching Strategy
|
|
|
|
### Branch Structure
|
|
|
|
SIA uses a simple branching strategy with two types of branches:
|
|
|
|
- **master**: The main branch containing stable, production-ready code
|
|
- **feature/{timestamp}_{description}**: Temporary branches for implementing specific improvements
|
|
|
|
This minimal approach is appropriate because:
|
|
- There is only one developer (SIA itself)
|
|
- Changes are typically focused on specific, well-defined improvements
|
|
- There's no need for parallel development streams
|
|
- Simplicity reduces the cognitive load on the agent
|
|
|
|
### Branch Naming
|
|
|
|
Feature branches follow a consistent naming convention:
|
|
```
|
|
feature/{YYYYMMDD}_{brief_description}
|
|
```
|
|
|
|
For example:
|
|
```
|
|
feature/20250115_improve_context_management
|
|
feature/20250203_add_email_tool
|
|
```
|
|
|
|
This convention:
|
|
- Makes it easy to identify when a branch was created
|
|
- Provides a clear indication of the branch's purpose
|
|
- Creates a chronological ordering when listing branches
|
|
- Avoids potential naming conflicts
|
|
|
|
## Commit Strategy
|
|
|
|
### Commit Frequency
|
|
|
|
SIA should commit changes before running tests.
|
|
This ensures that crashes of the core system can be traced back to a specific change.
|
|
|
|
### Commit Messages
|
|
|
|
Commit messages should follow a structured format:
|
|
|
|
```
|
|
{type}: {concise description}
|
|
|
|
{detailed explanation of changes and reasoning}
|
|
|
|
{reference to any relevant issues or test results}
|
|
```
|
|
|
|
Where `{type}` is one of:
|
|
- `core`: for changes on the core system
|
|
- `procedure`: for changes on procedures
|
|
- `test`: for changes on tests
|
|
- `tool`: for changes on tools
|
|
- `training`: for changes on training data
|
|
- `web`: for changes on the web interface
|
|
|
|
This structure:
|
|
- Makes it easy to understand the purpose of each commit
|
|
- Provides context for future review
|
|
- Creates a useful and navigable history
|
|
|
|
### Merge Strategy
|
|
|
|
The `--no-ff` flag creates a merge commit even for fast-forward merges, maintaining a clear record of the feature's development and completion.
|
|
|
|
## Credential Management
|
|
|
|
### Credential Storage
|
|
|
|
Git credentials are stored in the environment, not in the filesystem.
|
|
This ensures that credentials are not exposed in the iteration log.
|
|
Info about the repository and relevant environment variables is stored in `/root/data/environment/sia_repo.md`.
|
|
===== FILE: scripts/bootstrap.sh =====
|
|
#!/bin/bash
|
|
# bootstrap.sh - Initialize SIA (Self-Improving Agent) environment for cloud deployment
|
|
|
|
set -eo pipefail # Exit on any error, pipe failures
|
|
|
|
# Hardcoded paths for cloud deployment
|
|
SIA_REPO_URL="ssh://git@git.nielsgeens.be:222/llm/SIA.git"
|
|
SIA_DIR="/root/sia"
|
|
DATA_DIR="/root/data"
|
|
MODELS_DIR="/root/models"
|
|
DESKTOP_DIR="/root/desktop"
|
|
STATIC_DIR="/root/static"
|
|
VENVS_DIR="/root/venvs"
|
|
|
|
# Print header
|
|
echo "==================================================="
|
|
echo "SIA Bootstrap Script - Cloud Deployment"
|
|
echo "==================================================="
|
|
|
|
# Create directory structure
|
|
echo "Creating directory structure..."
|
|
mkdir -p "$DATA_DIR/iterations"
|
|
mkdir -p "$DESKTOP_DIR"
|
|
mkdir -p "$VENVS_DIR"
|
|
cd "$DESKTOP_DIR"
|
|
|
|
# Set up SSH keys
|
|
echo "Setting up SSH keys for git access..."
|
|
mkdir -p ~/.ssh
|
|
chmod 700 ~/.ssh
|
|
ssh-keygen -t sia_git -N "" -f ~/.ssh/sia_git -C "sia-agent"
|
|
echo "New SSH key generated"
|
|
|
|
# Display public key for user to add to git server
|
|
echo "==================================================="
|
|
echo "Add this public key to your git server:"
|
|
cat ~/.ssh/sia_git.pub
|
|
echo "==================================================="
|
|
|
|
# Prompt user to confirm they've added the key
|
|
read -p "Press Enter once you've added the SSH key to the git server..."
|
|
|
|
# Clone SIA repository
|
|
echo "Cloning SIA repository..."
|
|
git clone "$SIA_REPO_URL" "$SIA_DIR"
|
|
|
|
# Create and setup virtual environments
|
|
echo "Setting up SIA virtual environments..."
|
|
|
|
# Setup ITB tool environment
|
|
echo "Creating ITB tool environment..."
|
|
python3 -m venv "$VENVS_DIR/itb"
|
|
"$VENVS_DIR/itb/bin/pip" install -e "$SIA_DIR/tools/itb"
|
|
|
|
# Setup Train tool environment
|
|
echo "Creating Train tool environment..."
|
|
python3 -m venv "$VENVS_DIR/train"
|
|
"$VENVS_DIR/train/bin/pip" install -e "$SIA_DIR/tools/train"
|
|
|
|
# Setup SIA core environment
|
|
echo "Creating SIA core environment..."
|
|
python3 -m venv "$VENVS_DIR/sia"
|
|
"$VENVS_DIR/sia/bin/pip" install -e "$SIA_DIR"
|
|
|
|
# Build web interface
|
|
echo "Building web interface"
|
|
cd "$SIA_DIR/web"
|
|
|
|
# Install Node.js if needed
|
|
if ! command -v node &> /dev/null; then
|
|
echo "Installing Node.js..."
|
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
|
|
export NVM_DIR="$HOME/.nvm"
|
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
|
nvm install node
|
|
fi
|
|
|
|
npm install
|
|
npm run build
|
|
|
|
mkdir -p "$STATIC_DIR"
|
|
cp -r "$SIA_DIR/web/dist/"* "$STATIC_DIR/"
|
|
|
|
echo "Web interface built successfully"
|
|
|
|
# Finetune model
|
|
echo "Starting model finetuning..."
|
|
|
|
COMMIT_ID=$(cd "$SIA_DIR" && git rev-parse HEAD)
|
|
echo "Current commit: $COMMIT_ID"
|
|
|
|
mkdir -p "$MODELS_DIR/$COMMIT_ID"
|
|
mkdir -p "$MODELS_DIR/current"
|
|
|
|
# Run finetuning using the train environment
|
|
"$VENVS_DIR/train/bin/train_deepseek" --output-dir "$MODELS_DIR/$COMMIT_ID"
|
|
|
|
ln -sf "$MODELS_DIR/$COMMIT_ID" "$MODELS_DIR/current"
|
|
echo "Finetuning complete, model linked to current"
|
|
|
|
# Initialize environment information
|
|
echo "Initializing environment information..."
|
|
mkdir -p "$DATA_DIR/environment"
|
|
cat > "$DATA_DIR/environment/sia_repo.md" << EOF
|
|
# SIA Repository Information
|
|
|
|
- Repository URL: $SIA_REPO_URL
|
|
- ssh key: ~/.ssh/sia_git.pub
|
|
EOF
|
|
|
|
# Create .env file for local model only
|
|
cat > "$SIA_DIR/.env" << EOF
|
|
SIA_DEEPSEEK_ENABLED=true
|
|
SIA_DEEPSEEK_MODEL=$MODELS_DIR/current
|
|
SIA_DEEPSEEK_TEMPERATURE=0.6
|
|
EOF
|
|
|
|
# Print header
|
|
echo "==================================================="
|
|
echo "SIA environment initialization complete!"
|
|
echo "==================================================="
|
|
|
|
# Start SIA using restart script
|
|
echo "Starting SIA..."
|
|
"$SIA_DIR/scripts/restart.sh"
|
|
===== FILE: scripts/collect.sh =====
|
|
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
declare -A FILTER_SETS=(
|
|
["py"]="-f .*(\\.py|requirements.txt)$"
|
|
["web"]="-f .*\\.(js|jsx|json|css|html)$"
|
|
["doc"]="-f .*\\.md$"
|
|
["deploy"]="-f .*(Dockerfile|\\.sh|\\.xsd|\\.yaml)$"
|
|
|
|
["core"]="-s py ./sia ./tools -s deploy . -f ^(?!procedures/).*\\.md$ ."
|
|
["webui"]="-s web ./web"
|
|
["tests"]="-s py ./test"
|
|
["procedures"]="-s doc ./procedures"
|
|
)
|
|
|
|
OUTPUT="/dev/stdout"
|
|
VERBOSE=0
|
|
|
|
usage() {
|
|
echo "Usage: $0 [OPTIONS] [DIRS...]"
|
|
echo
|
|
echo "Options:"
|
|
echo " -f, --filter PATTERN Regex filter for subsequent directories"
|
|
echo " -s, --set SETNAME Use predefined set of parameters"
|
|
echo " -o, --output FILE Output file (default: stdout)"
|
|
echo " -v, -vv Verbose output"
|
|
echo
|
|
echo "Predefined Filter Sets:"
|
|
for setname in $(echo "${!FILTER_SETS[@]}" | tr ' ' '\n' | sort); do
|
|
printf " %-15s %s\n" "$setname:" "${FILTER_SETS[$setname]}"
|
|
done
|
|
exit 1
|
|
}
|
|
|
|
# Expand any set arguments to their full form
|
|
expand_args() {
|
|
local -a expanded=()
|
|
local i=1
|
|
local has_sets=0
|
|
|
|
# First pass: expand sets
|
|
while [ $i -le $# ]; do
|
|
local arg="${!i}"
|
|
|
|
case "$arg" in
|
|
-s|--set)
|
|
has_sets=1
|
|
i=$((i+1))
|
|
[ $i -le $# ] || { echo "Error: No set name specified"; usage; }
|
|
local set_name="${!i}"
|
|
|
|
if [[ -v "FILTER_SETS[$set_name]" ]]; then
|
|
# Add the expanded set parameters
|
|
local -a set_args
|
|
read -ra set_args <<< "${FILTER_SETS[$set_name]}"
|
|
for set_arg in "${set_args[@]}"; do
|
|
expanded+=("$set_arg")
|
|
done
|
|
else
|
|
echo "Error: Unknown set '$set_name'"
|
|
usage
|
|
fi
|
|
;;
|
|
*)
|
|
# Preserve other arguments
|
|
expanded+=("$arg")
|
|
;;
|
|
esac
|
|
|
|
i=$((i+1))
|
|
done
|
|
|
|
# If we expanded sets, recursively expand again until no more sets
|
|
if [ $has_sets -eq 1 ]; then
|
|
expand_args "${expanded[@]}"
|
|
else
|
|
# No more sets, return the fully expanded arguments
|
|
echo "${expanded[@]+"${expanded[@]}"}"
|
|
fi
|
|
}
|
|
|
|
# Process the final expanded command line
|
|
process_command() {
|
|
local filter=""
|
|
declare -A dir_filters_map # Maps directory to array of filters
|
|
|
|
if ((VERBOSE >= 1)); then
|
|
echo "Expanded arguments:"
|
|
for arg in "$@"; do
|
|
echo " $arg"
|
|
done
|
|
fi
|
|
|
|
# Process final arguments
|
|
local i=1
|
|
while [ $i -le $# ]; do
|
|
local arg="${!i}"
|
|
|
|
case "$arg" in
|
|
-f|--filter)
|
|
i=$((i+1))
|
|
[ $i -le $# ] || break
|
|
filter="${!i}"
|
|
;;
|
|
-o|--output)
|
|
i=$((i+1))
|
|
[ $i -le $# ] || break
|
|
OUTPUT="${!i}"
|
|
;;
|
|
-v)
|
|
((VERBOSE++))
|
|
;;
|
|
-vv)
|
|
VERBOSE=2
|
|
;;
|
|
-s|--set)
|
|
echo "Warning: Set argument found after expansion"
|
|
i=$((i+1))
|
|
;;
|
|
-*)
|
|
# Skip other options
|
|
;;
|
|
*)
|
|
# It's a directory
|
|
if [ -e "$arg" ]; then
|
|
# Add filter to this directory's filter list
|
|
if [ -n "$filter" ]; then
|
|
if [ -n "${dir_filters_map[$arg]:-}" ]; then
|
|
dir_filters_map["$arg"]="${dir_filters_map["$arg"]}|$filter"
|
|
else
|
|
dir_filters_map["$arg"]="$filter"
|
|
fi
|
|
if ((VERBOSE >= 1)); then
|
|
echo "Adding filter: $filter to directory: $arg"
|
|
fi
|
|
else
|
|
# If no filter specified, ensure directory is in the map
|
|
[ -z "${dir_filters_map[$arg]:-}" ] && dir_filters_map["$arg"]=""
|
|
if ((VERBOSE >= 1)); then
|
|
echo "Adding directory with no filter: $arg"
|
|
fi
|
|
fi
|
|
else
|
|
echo "Warning: Directory '$arg' not found"
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
i=$((i+1))
|
|
done
|
|
|
|
# Convert output path and check conflicts
|
|
output_abs_path=$(realpath -m "$OUTPUT")
|
|
output_rel_path=$(realpath --relative-to=. "$output_abs_path")
|
|
|
|
# Generate directory tree
|
|
{
|
|
echo "Directory Tree:"
|
|
if command -v tree &>/dev/null; then
|
|
tree -a -I '.git' .
|
|
else
|
|
find . -name .git -prune -o -print | sed -e 's;[^/]*/;|____;g;s;____|; |;g'
|
|
fi
|
|
} > "$OUTPUT"
|
|
|
|
# Process directories and collect files
|
|
declare -a all_files=()
|
|
|
|
# Cache git-ignored files once
|
|
declare -a git_ignored_files=()
|
|
if git rev-parse --is-inside-work-tree &>/dev/null; then
|
|
while IFS= read -r ignored_file; do
|
|
git_ignored_files+=("$ignored_file")
|
|
done < <(git ls-files --others --ignored --exclude-standard 2>/dev/null)
|
|
|
|
if ((VERBOSE >= 1)); then
|
|
echo "Cached ${#git_ignored_files[@]} git-ignored files"
|
|
fi
|
|
fi
|
|
|
|
for dir in "${!dir_filters_map[@]}"; do
|
|
local filters="${dir_filters_map[$dir]}"
|
|
|
|
if ((VERBOSE >= 1)); then
|
|
echo "Processing directory: $dir with filters: ${filters:-none}"
|
|
fi
|
|
|
|
# Get all files in the directory first
|
|
declare -a dir_files=()
|
|
while IFS= read -r -d $'\0' file; do
|
|
local rel_path="${file#./}"
|
|
[[ -z "$rel_path" || "$rel_path" == "." ]] && continue
|
|
|
|
# Skip output file
|
|
[[ "$rel_path" == "$output_rel_path" ]] && continue
|
|
|
|
# Check if file is git-ignored using cached list
|
|
local is_ignored=0
|
|
for ignored in "${git_ignored_files[@]}"; do
|
|
if [[ "$rel_path" == "$ignored" ]]; then
|
|
is_ignored=1
|
|
break
|
|
fi
|
|
done
|
|
|
|
# Skip git-ignored files
|
|
[[ $is_ignored -eq 1 ]] && continue
|
|
|
|
dir_files+=("$rel_path")
|
|
done < <(find "$dir" \( -path '*/.git' -prune \) -o \( -path "./$output_rel_path" -prune \) -o -type f -print0 2>/dev/null)
|
|
|
|
if ((VERBOSE >= 1)); then
|
|
echo " Found ${#dir_files[@]} files in directory"
|
|
fi
|
|
|
|
# If filters are specified, apply them to the full file list
|
|
if [[ -n "$filters" ]]; then
|
|
# Create temp file with all paths
|
|
local temp_file=$(mktemp)
|
|
printf "%s\n" "${dir_files[@]}" > "$temp_file"
|
|
|
|
# Apply filters to get matching files
|
|
declare -a matching_files=()
|
|
while IFS= read -r matched; do
|
|
[[ -n "$matched" ]] && matching_files+=("$matched")
|
|
(( VERBOSE >= 2 )) && echo " Included: $matched"
|
|
done < <(grep -E "($filters)" "$temp_file")
|
|
|
|
# Add matching files to all_files
|
|
all_files+=("${matching_files[@]}")
|
|
|
|
# Clean up
|
|
rm -f "$temp_file"
|
|
|
|
if ((VERBOSE >= 1)); then
|
|
echo " Matched ${#matching_files[@]} files after filtering"
|
|
fi
|
|
else
|
|
# No filter, add all files
|
|
all_files+=("${dir_files[@]}")
|
|
|
|
if ((VERBOSE >= 2)); then
|
|
for file in "${dir_files[@]}"; do
|
|
echo " Included: $file"
|
|
done
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Generate unique file list
|
|
if [ ${#all_files[@]} -gt 0 ]; then
|
|
readarray -t unique_files < <(printf "%s\n" "${all_files[@]}" | sort -u)
|
|
|
|
# Append file contents to output
|
|
{
|
|
echo -e "\n\nFile Contents:"
|
|
for file in "${unique_files[@]}"; do
|
|
if [[ -L "$file" ]]; then
|
|
target=$(readlink -f "$file" || echo "unknown")
|
|
echo -e "\n===== SYMLINK: $file → $target ====="
|
|
else
|
|
echo -e "\n===== FILE: $file ====="
|
|
cat "$file" 2>/dev/null || echo "Error: Unable to read file"
|
|
fi
|
|
done
|
|
} >> "$OUTPUT"
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
# No arguments? Show usage
|
|
[ $# -eq 0 ] && usage
|
|
|
|
# First scan for verbose flags
|
|
for arg in "$@"; do
|
|
if [[ "$arg" == "-v" ]]; then
|
|
((VERBOSE++))
|
|
elif [[ "$arg" == "-vv" ]]; then
|
|
VERBOSE=2
|
|
fi
|
|
done
|
|
|
|
# Expand all set arguments recursively
|
|
local -a expanded_args
|
|
read -ra expanded_args <<< "$(expand_args "$@")"
|
|
|
|
# Process the completely expanded arguments
|
|
process_command "${expanded_args[@]}"
|
|
|
|
echo "Concatenation complete. Output written to $OUTPUT" >&2
|
|
}
|
|
|
|
main "$@"
|
|
|
|
===== FILE: scripts/container.sh =====
|
|
#!/bin/bash
|
|
|
|
container_id=$(docker ps -q)
|
|
docker exec -it $container_id bash
|
|
===== FILE: scripts/restart.sh =====
|
|
#!/bin/bash
|
|
|
|
while true; do
|
|
sia
|
|
if [ $? -eq 42 ]; then
|
|
echo "SIA exited with code 42. Restarting."
|
|
else
|
|
echo "SIA exited with code $?. Not restarting."
|
|
break
|
|
fi
|
|
done
|
|
===== FILE: scripts/run.sh =====
|
|
#!/bin/bash
|
|
|
|
export MSYS_NO_PATHCONV=1
|
|
set -e
|
|
|
|
function chown_iterations() {
|
|
if [ -d "./iterations" ] && [ "$(find ./iterations/ ! -user $USER -o ! -group $USER 2>/dev/null)" ]; then
|
|
echo "Chowning iterations directory"
|
|
sudo chown -R $USER:$USER ./iterations/
|
|
fi
|
|
}
|
|
|
|
trap chown_iterations EXIT
|
|
|
|
docker build \
|
|
--tag sia \
|
|
.
|
|
|
|
docker run \
|
|
--init \
|
|
--rm \
|
|
-ti \
|
|
--gpus=all \
|
|
-p 8080:8080 \
|
|
--env-file .env \
|
|
-v /$(pwd)/model/:/root/models/current/ \
|
|
-v /$(pwd)/iterations/:/root/data/iterations/ \
|
|
-v /$(pwd)/tasks/:/root/data/tasks/ \
|
|
-v /$(pwd)/user/:/root/data/user/ \
|
|
-v /$(pwd)/environment/:/root/data/environment/ \
|
|
-v /$(pwd)/:/root/sia/ \
|
|
sia "$@"
|
|
|
|
exit $?
|
|
===== FILE: scripts/test.sh =====
|
|
#!/bin/bash
|
|
|
|
docker build \
|
|
--tag sia \
|
|
.
|
|
|
|
# Run tests within the SIA virtual environment
|
|
docker run \
|
|
--rm \
|
|
-ti \
|
|
--gpus=all \
|
|
-p 8080:8080 \
|
|
--env-file .env \
|
|
-v /$(pwd)/model/:/root/models/current/ \
|
|
-v /$(pwd)/iterations/:/root/data/iterations/ \
|
|
-v /$(pwd)/tasks/:/root/data/tasks/ \
|
|
-v /$(pwd)/user/:/root/data/user/ \
|
|
-v /$(pwd)/environment/:/root/data/environment/ \
|
|
-v /$(pwd)/:/root/sia/ \
|
|
sia /root/venvs/sia/bin/python -m unittest discover -v -p "*test.py"
|
|
===== FILE: sia/__init__.py =====
|
|
|
|
===== FILE: sia/__main__.py =====
|
|
from aiohttp import web
|
|
import asyncio
|
|
|
|
from .auto_approver import AutoApprover
|
|
from .config import Config
|
|
from .llm_engine.hf_llm_engine import HfLlmEngine
|
|
from .llm_engine.deepseek_llm_engine import DeepSeekLlmEngine
|
|
from .iteration_logger import IterationLogger
|
|
from .llm_engine.local_llm_engine import LocalLlmEngine
|
|
from .llm_engine.mistral_llm_engine import MistralLlmEngine
|
|
from .llm_engine.openai_llm_engine import OpenAILlmEngine
|
|
from .response_parser import ResponseParser
|
|
from .system_metrics import SystemMetrics
|
|
from .web.api import Api
|
|
from .web.static import Static
|
|
from .web.websockts 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
|
|
async def create(cls, config: Config):
|
|
self = cls()
|
|
self._config = config
|
|
|
|
self._system_prompt = self._config.system_prompt.read_text()
|
|
self._action_schema = self._config.action_schema.read_text()
|
|
|
|
# Initialize LLM engines based on config
|
|
self._llms = {}
|
|
|
|
if config.local_enabled:
|
|
self._llms['local'] = LocalLlmEngine(
|
|
config.local_model,
|
|
config.local_temperature,
|
|
config.local_token_limit,
|
|
config.local_api_key,
|
|
)
|
|
|
|
if config.openai_enabled:
|
|
self._llms['openai'] = OpenAILlmEngine(
|
|
config.openai_model,
|
|
config.openai_temperature,
|
|
config.openai_token_limit,
|
|
config.openai_api_key,
|
|
)
|
|
|
|
if config.hf_enabled:
|
|
self._llms['hf'] = HfLlmEngine(
|
|
config.hf_model,
|
|
config.hf_temperature,
|
|
config.hf_api_key,
|
|
)
|
|
|
|
if config.mistral_enabled:
|
|
self._llms['mistral'] = MistralLlmEngine(
|
|
config.mistral_model,
|
|
config.mistral_temperature,
|
|
config.mistral_token_limit,
|
|
config.mistral_api_key,
|
|
)
|
|
|
|
if config.deepseek_enabled:
|
|
self._llms['deepseek'] = DeepSeekLlmEngine(
|
|
config.deepseek_model,
|
|
config.deepseek_temperature,
|
|
config.deepseek_token_limit,
|
|
config.hf_api_key, # Use the existing HF API key
|
|
)
|
|
|
|
if not self._llms:
|
|
raise ValueError("No LLM engines enabled in configuration")
|
|
|
|
self._io_buffer = WebIOBuffer()
|
|
self._working_memory = WorkingMemory()
|
|
self._agent = WebAgent(
|
|
system_prompt=self._system_prompt,
|
|
action_schema=self._action_schema,
|
|
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),
|
|
)
|
|
self._auto_approver = AutoApprover(self._agent)
|
|
|
|
self._app = web.Application()
|
|
self._api = Api(config.work_dir, self._app, self._agent, self._io_buffer, self._working_memory, self._auto_approver)
|
|
self._websockets = Websockets(self._app, self._agent, self._io_buffer, self._auto_approver, self._working_memory)
|
|
self._static = Static(self._app, self._config)
|
|
|
|
return self
|
|
|
|
@property
|
|
def app(self):
|
|
return self._app
|
|
|
|
async def _serve_index(self, request: web.Request) -> web.Response:
|
|
"""Serve the React application HTML for any unmatched routes."""
|
|
index_path = self._config.static_files / "index.html"
|
|
if not index_path.exists():
|
|
raise web.HTTPNotFound()
|
|
|
|
with open(index_path, "r") as f:
|
|
html_content = f.read()
|
|
|
|
return web.Response(
|
|
text=html_content,
|
|
content_type="text/html"
|
|
)
|
|
|
|
def main():
|
|
loop = asyncio.new_event_loop()
|
|
config = Config()
|
|
main_instance = loop.run_until_complete(Main.create(config))
|
|
print(f"Web server started at http://localhost:{config.port}")
|
|
web.run_app(main_instance.app, loop=loop, host=config.host, port=config.port)
|
|
return 0
|
|
===== FILE: sia/auto_approver.py =====
|
|
from threading import Thread, Event
|
|
from typing import Callable, TypeAlias, TypedDict
|
|
from .web_agent import WebAgent, LlmState
|
|
|
|
class AutoApproverConfig(TypedDict):
|
|
context_enabled: bool
|
|
response_enabled: bool
|
|
context_timeout: float
|
|
response_timeout: float
|
|
llm_name: str
|
|
|
|
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._llm_name = next(iter(agent.llms.keys()))
|
|
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_llm_change_handler(self._handle_llm_state_change)
|
|
self.agent.add_context_change_handler(self._handle_context_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,
|
|
llm_name=self._llm_name
|
|
)
|
|
|
|
def set_config(self, config: AutoApproverConfig) -> None:
|
|
if config['llm_name'] not in self.agent.llms:
|
|
raise ValueError(f"Unknown LLM: {config['llm_name']}")
|
|
|
|
notify_config_change = self._notify_config_change
|
|
self._notify_config_change = lambda: None
|
|
|
|
try:
|
|
self.context_enabled = False
|
|
self.response_enabled = False
|
|
|
|
self.context_timeout = config['context_timeout']
|
|
self.response_timeout = config['response_timeout']
|
|
self.llm_name = config['llm_name']
|
|
|
|
self.context_enabled = config['context_enabled']
|
|
self.response_enabled = config['response_enabled']
|
|
finally:
|
|
self._notify_config_change = notify_config_change
|
|
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.llms[self._llm_name] == LlmState.NO_OUTPUT:
|
|
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.llms[self._llm_name] == LlmState.OUTPUT:
|
|
self._start_response_thread()
|
|
self._notify_config_change()
|
|
|
|
@property
|
|
def llm_name(self) -> str:
|
|
return self._llm_name
|
|
|
|
@llm_name.setter
|
|
def llm_name(self, name: str) -> None:
|
|
if name not in self.agent.llms:
|
|
raise ValueError(f"Unknown LLM: {name}")
|
|
self._llm_name = name
|
|
self._notify_config_change()
|
|
|
|
def _handle_llm_state_change(self, llm_name: str, state: LlmState) -> None:
|
|
if llm_name != self._llm_name:
|
|
return
|
|
|
|
if state == LlmState.OUTPUT and self._response_enabled:
|
|
self._start_response_thread()
|
|
else:
|
|
self._stop_response_thread()
|
|
|
|
def _handle_context_change(self, context: str, generated: bool) -> None:
|
|
if generated and self._context_enabled:
|
|
self._start_context_thread()
|
|
else:
|
|
self._stop_context_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:
|
|
self.agent.run_inference(self._llm_name)
|
|
|
|
def _response_approval_thread(self) -> None:
|
|
if self._stop_event.wait(self._response_timeout):
|
|
return
|
|
if (self._response_enabled and
|
|
self.agent.llms[self._llm_name] == LlmState.OUTPUT):
|
|
self.agent.approve_response(self._llm_name, self.agent.get_output(self._llm_name))
|
|
|
|
===== FILE: sia/base_agent.py =====
|
|
from abc import ABC
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from .llm_engine import LlmEngine
|
|
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):
|
|
"""
|
|
Abstract base class for SIA agents.
|
|
|
|
Provides core functionality for maintaining working memory, system metrics,
|
|
and coordinating components for LLM inference.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
system_prompt: str,
|
|
action_schema: str,
|
|
working_memory: WorkingMemory,
|
|
metrics: SystemMetrics,
|
|
validator: XMLValidator,
|
|
parser: ResponseParser,
|
|
):
|
|
"""
|
|
Initialize agent with required components.
|
|
"""
|
|
self._system_prompt = system_prompt
|
|
self._action_schema = action_schema
|
|
self._working_memory = working_memory
|
|
self._metrics = metrics
|
|
self._validator = validator
|
|
self._parser = parser
|
|
|
|
@property
|
|
def system_prompt(self) -> str:
|
|
"""Get the system prompt."""
|
|
return f"{self._system_prompt}\n{self._action_schema}"
|
|
|
|
def _compile_context(self, llmEngine: LlmEngine) -> str:
|
|
"""
|
|
Compile the current context for LLM inference.
|
|
Includes system metrics and working memory entries.
|
|
|
|
Returns:
|
|
str: Complete context as XML string
|
|
"""
|
|
memory_context = self._working_memory.generate_context()
|
|
metrics_data = self._metrics.get_metrics()
|
|
|
|
# Create context element
|
|
context = ET.Element("context")
|
|
context.set("time", metrics_data["timestamp"])
|
|
context.set("memory_used", str(metrics_data["memory_used"]))
|
|
context.set("memory_total", str(metrics_data["memory_total"]))
|
|
context.set("disk_used", str(metrics_data["disk_used"]))
|
|
context.set("disk_total", str(metrics_data["disk_total"]))
|
|
context.set("stdin", str(self._parser.io_buffer.buffer_length()))
|
|
context.set("context", "100%")
|
|
|
|
for entry in memory_context:
|
|
context.append(entry)
|
|
|
|
context_str = pretty_print_element(context)
|
|
|
|
# Calculate token usage percentage
|
|
token_count = llmEngine.token_count(self.system_prompt, context_str)
|
|
token_limit = llmEngine.token_limit()
|
|
context_usage = (float(token_count) / float(token_limit)) * 100.0
|
|
|
|
# Update context usage metric
|
|
context.set("context", f"{str(round(context_usage, 2))}%")
|
|
|
|
return pretty_print_element(context)
|
|
===== FILE: sia/command.py =====
|
|
from abc import ABC, abstractmethod
|
|
from typing import TYPE_CHECKING
|
|
|
|
from .working_memory import WorkingMemory
|
|
from .command_result import CommandResult
|
|
|
|
class Command(ABC):
|
|
"""
|
|
Abstract base class for all commands that can be executed on working memory.
|
|
Commands represent immediate actions that modify the system state.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def execute(self, memory: WorkingMemory) -> CommandResult:
|
|
"""
|
|
Execute the command on the given working memory.
|
|
|
|
Args:
|
|
memory: WorkingMemory instance to execute command on
|
|
|
|
Returns:
|
|
CommandResult: Result of command execution
|
|
"""
|
|
pass
|
|
|
|
===== FILE: sia/command_result.py =====
|
|
from dataclasses import dataclass
|
|
|
|
@dataclass
|
|
class CommandResult:
|
|
"""
|
|
Result of a command execution.
|
|
|
|
Attributes:
|
|
message: Optional message describing the result
|
|
success: Whether the command executed successfully
|
|
should_stop: Whether the agent should stop after this command
|
|
"""
|
|
message: str
|
|
success: bool = True
|
|
should_stop: bool = False
|
|
|
|
@staticmethod
|
|
def success() -> 'CommandResult':
|
|
"""Create a successful command result."""
|
|
return CommandResult(message="", success=True, should_stop=False)
|
|
|
|
@staticmethod
|
|
def failure(message: str) -> 'CommandResult':
|
|
"""
|
|
Create a failed command result.
|
|
|
|
Args:
|
|
message: Description of the failure
|
|
"""
|
|
return CommandResult(message=message, success=False, should_stop=False)
|
|
|
|
@staticmethod
|
|
def stop() -> 'CommandResult':
|
|
"""Create a command result indicating the agent should stop."""
|
|
return CommandResult(message="", success=True, should_stop=True)
|
|
===== FILE: sia/config.py =====
|
|
from dataclasses import dataclass
|
|
from python_dotenv import load_dotenv
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
import argparse
|
|
import os
|
|
|
|
@dataclass
|
|
class Config:
|
|
def __init__(self):
|
|
load_dotenv()
|
|
parser = argparse.ArgumentParser(description='SIA - Self Improving Agent')
|
|
|
|
# Core configuration
|
|
parser.add_argument(
|
|
'--system-prompt',
|
|
type=Path,
|
|
default=os.getenv('SIA_SYSTEM_PROMPT', '/root/sia/system_prompt.md'),
|
|
help='Path to the system prompt file (default: /root/sia/system_prompt.md, env: SIA_SYSTEM_PROMPT)'
|
|
)
|
|
parser.add_argument(
|
|
'--action-schema',
|
|
type=Path,
|
|
default=os.getenv('SIA_ACTION_SCHEMA', '/root/sia/action_schema.xsd'),
|
|
help='Path to the action schema file (default: /root/sia/action_schema.xsd, env: SIA_ACTION_SCHEMA)'
|
|
)
|
|
parser.add_argument(
|
|
'--iterations-dir',
|
|
type=Path,
|
|
default=os.getenv('SIA_ITERATIONS_DIR', '/root/data/iterations'),
|
|
help='Path to the directory for storing iterations (default: /root/data/iterations, env: SIA_ITERATIONS_DIR)'
|
|
)
|
|
parser.add_argument(
|
|
'--work-dir',
|
|
type=Path,
|
|
default=os.getenv('SIA_WORK_DIR', '/root/desktop'),
|
|
help='Path to the working directory (default: /root/desktop, env: SIA_WORK_DIR)'
|
|
)
|
|
|
|
# Web server configuration
|
|
parser.add_argument(
|
|
'--server',
|
|
action='store_true',
|
|
default=self._parse_bool_env('SIA_SERVER_ENABLED', False),
|
|
help='Enable web server for debugging and human feedback (env: SIA_SERVER_ENABLED)'
|
|
)
|
|
parser.add_argument(
|
|
'--host',
|
|
type=str,
|
|
default=os.getenv('SIA_SERVER_HOST', '0.0.0.0'),
|
|
help='Web server host (default: 0.0.0.0, env: SIA_SERVER_HOST)'
|
|
)
|
|
parser.add_argument(
|
|
'--port',
|
|
type=int,
|
|
default=int(os.getenv('SIA_SERVER_PORT', '8080')),
|
|
help='Web server port (default: 8080, env: SIA_SERVER_PORT)'
|
|
)
|
|
parser.add_argument(
|
|
'--static-files',
|
|
type=Path,
|
|
default=self._parse_optional_path('SIA_STATIC_FILES', '/root/static/'),
|
|
help='Path to static web files (default: /root/static/, env: SIA_STATIC_FILES)'
|
|
)
|
|
|
|
# Local LLM configuration
|
|
parser.add_argument(
|
|
'--local-enable',
|
|
action='store_true',
|
|
default=self._parse_bool_env('SIA_LOCAL_ENABLED', False),
|
|
help='Enable local LLM engine (env: SIA_LOCAL_ENABLED)'
|
|
)
|
|
parser.add_argument(
|
|
'--local-model',
|
|
type=str,
|
|
default=os.getenv('SIA_LOCAL_MODEL', '/root/models/current'),
|
|
help='Path to local model directory (default: /root/models/current, env: SIA_LOCAL_MODEL)'
|
|
)
|
|
parser.add_argument(
|
|
'--local-temperature',
|
|
type=float,
|
|
default=float(os.getenv('SIA_LOCAL_TEMPERATURE', '0.7')),
|
|
help='Local LLM temperature (default: 0.7, env: SIA_LOCAL_TEMPERATURE)'
|
|
)
|
|
parser.add_argument(
|
|
'--local-token-limit',
|
|
type=int,
|
|
default=int(os.getenv('SIA_LOCAL_TOKEN_LIMIT', '2048')),
|
|
help='Local LLM token limit (env: SIA_LOCAL_TOKEN_LIMIT)'
|
|
)
|
|
parser.add_argument(
|
|
'--local-api-key',
|
|
type=str,
|
|
default=os.getenv('SIA_LOCAL_API_KEY'),
|
|
help='API key for local models (env: SIA_LOCAL_API_KEY)'
|
|
)
|
|
|
|
# OpenAI configuration
|
|
parser.add_argument(
|
|
'--openai-enable',
|
|
action='store_true',
|
|
default=self._parse_bool_env('SIA_OPENAI_ENABLED', False),
|
|
help='Enable OpenAI LLM engine (env: SIA_OPENAI_ENABLED)'
|
|
)
|
|
parser.add_argument(
|
|
'--openai-model',
|
|
type=str,
|
|
default=os.getenv('SIA_OPENAI_MODEL', 'gpt-3.5-turbo'),
|
|
help='OpenAI model name (default: gpt-3.5-turbo, env: SIA_OPENAI_MODEL)'
|
|
)
|
|
parser.add_argument(
|
|
'--openai-temperature',
|
|
type=float,
|
|
default=float(os.getenv('SIA_OPENAI_TEMPERATURE', '0.7')),
|
|
help='OpenAI temperature (default: 0.7, env: SIA_OPENAI_TEMPERATURE)'
|
|
)
|
|
parser.add_argument(
|
|
'--openai-token-limit',
|
|
type=int,
|
|
default=int(os.getenv('SIA_OPENAI_TOKEN_LIMIT', '4096')),
|
|
help='OpenAI token limit (env: SIA_OPENAI_TOKEN_LIMIT)'
|
|
)
|
|
parser.add_argument(
|
|
'--openai-api-key',
|
|
type=str,
|
|
default=os.getenv('SIA_OPENAI_API_KEY'),
|
|
help='OpenAI API key (env: SIA_OPENAI_API_KEY)'
|
|
)
|
|
|
|
# Hugging Face configuration
|
|
parser.add_argument(
|
|
'--hf-enable',
|
|
action='store_true',
|
|
default=self._parse_bool_env('SIA_HF_ENABLED', False),
|
|
help='Enable Hugging Face LLM engine (env: SIA_HF_ENABLED)'
|
|
)
|
|
parser.add_argument(
|
|
'--hf-model',
|
|
type=str,
|
|
default=os.getenv('SIA_HF_MODEL'),
|
|
help='Hugging Face model name (env: SIA_HF_MODEL)'
|
|
)
|
|
parser.add_argument(
|
|
'--hf-temperature',
|
|
type=float,
|
|
default=float(os.getenv('SIA_HF_TEMPERATURE', '0.7')),
|
|
help='Hugging Face temperature (default: 0.7, env: SIA_HF_TEMPERATURE)'
|
|
)
|
|
parser.add_argument(
|
|
'--hf-api-key',
|
|
type=str,
|
|
default=os.getenv('SIA_HF_API_KEY'),
|
|
help='Hugging Face API key (env: SIA_HF_API_KEY)'
|
|
)
|
|
|
|
# Mistral configuration
|
|
parser.add_argument(
|
|
'--mistral-enable',
|
|
action='store_true',
|
|
default=self._parse_bool_env('SIA_MISTRAL_ENABLED', False),
|
|
help='Enable Mistral LLM engine (env: SIA_MISTRAL_ENABLED)'
|
|
)
|
|
parser.add_argument(
|
|
'--mistral-model',
|
|
type=str,
|
|
default=os.getenv('SIA_MISTRAL_MODEL'),
|
|
help='Mistral model name (env: SIA_MISTRAL_MODEL)'
|
|
)
|
|
parser.add_argument(
|
|
'--mistral-temperature',
|
|
type=float,
|
|
default=float(os.getenv('SIA_MISTRAL_TEMPERATURE', '0.7')),
|
|
help='Mistral temperature (default: 0.7, env: SIA_MISTRAL_TEMPERATURE)'
|
|
)
|
|
parser.add_argument(
|
|
'--mistral-token-limit',
|
|
type=int,
|
|
default=int(os.getenv('SIA_MISTRAL_TOKEN_LIMIT', '4096')),
|
|
help='Mistral token limit (env: SIA_MISTRAL_TOKEN_LIMIT)'
|
|
)
|
|
parser.add_argument(
|
|
'--mistral-api-key',
|
|
type=str,
|
|
default=os.getenv('SIA_MISTRAL_API_KEY'),
|
|
help='Mistral API key (env: SIA_MISTRAL_API_KEY)'
|
|
)
|
|
parser.add_argument(
|
|
'--deepseek-enable',
|
|
action='store_true',
|
|
default=self._parse_bool_env('SIA_DEEPSEEK_ENABLED', False),
|
|
help='Enable DeepSeek LLM engine (env: SIA_DEEPSEEK_ENABLED)'
|
|
)
|
|
parser.add_argument(
|
|
'--deepseek-model',
|
|
type=str,
|
|
default=os.getenv('SIA_DEEPSEEK_MODEL', '/root/models/current'),
|
|
help='Path to fine-tuned DeepSeek model (env: SIA_DEEPSEEK_MODEL)'
|
|
)
|
|
parser.add_argument(
|
|
'--deepseek-temperature',
|
|
type=float,
|
|
default=float(os.getenv('SIA_DEEPSEEK_TEMPERATURE', '0.6')),
|
|
help='DeepSeek temperature (default: 0.6, env: SIA_DEEPSEEK_TEMPERATURE)'
|
|
)
|
|
parser.add_argument(
|
|
'--deepseek-token-limit',
|
|
type=int,
|
|
default=int(os.getenv('SIA_DEEPSEEK_TOKEN_LIMIT', '0')),
|
|
help='DeepSeek token limit (0 for model default, env: SIA_DEEPSEEK_TOKEN_LIMIT)'
|
|
)
|
|
|
|
self.args = parser.parse_args()
|
|
|
|
def _parse_bool_env(self, env_var: str, default: bool) -> bool:
|
|
val = os.getenv(env_var)
|
|
if val is None:
|
|
return default
|
|
return val.lower() in ('true', '1', 'yes', 'on')
|
|
|
|
def _parse_optional_path(self, env_var: str, default: Optional[Path]) -> Optional[Path]:
|
|
val = os.getenv(env_var)
|
|
if val is None:
|
|
return default
|
|
return Path(val)
|
|
|
|
# Core properties
|
|
@property
|
|
def system_prompt(self) -> Path:
|
|
return self.args.system_prompt
|
|
|
|
@property
|
|
def action_schema(self) -> Path:
|
|
return self.args.action_schema
|
|
|
|
@property
|
|
def iterations_dir(self) -> Path:
|
|
return self.args.iterations_dir
|
|
|
|
@property
|
|
def work_dir(self) -> Path:
|
|
return self.args.work_dir
|
|
|
|
# Server properties
|
|
@property
|
|
def server(self) -> bool:
|
|
return self.args.server
|
|
|
|
@property
|
|
def host(self) -> str:
|
|
return self.args.host
|
|
|
|
@property
|
|
def port(self) -> int:
|
|
return self.args.port
|
|
|
|
@property
|
|
def static_files(self) -> Path:
|
|
return self.args.static_files
|
|
|
|
# Local LLM properties
|
|
@property
|
|
def local_enabled(self) -> bool:
|
|
return self.args.local_enable
|
|
|
|
@property
|
|
def local_model(self) -> str:
|
|
return self.args.local_model
|
|
|
|
@property
|
|
def local_temperature(self) -> float:
|
|
return self.args.local_temperature
|
|
|
|
@property
|
|
def local_token_limit(self) -> int:
|
|
return self.args.local_token_limit
|
|
|
|
@property
|
|
def local_api_key(self) -> Optional[str]:
|
|
return self.args.local_api_key
|
|
|
|
# OpenAI properties
|
|
@property
|
|
def openai_enabled(self) -> bool:
|
|
return self.args.openai_enable
|
|
|
|
@property
|
|
def openai_model(self) -> str:
|
|
return self.args.openai_model
|
|
|
|
@property
|
|
def openai_temperature(self) -> float:
|
|
return self.args.openai_temperature
|
|
|
|
@property
|
|
def openai_token_limit(self) -> int:
|
|
return self.args.openai_token_limit
|
|
|
|
@property
|
|
def openai_api_key(self) -> Optional[str]:
|
|
return self.args.openai_api_key
|
|
|
|
# Hugging Face properties
|
|
@property
|
|
def hf_enabled(self) -> bool:
|
|
return self.args.hf_enable
|
|
|
|
@property
|
|
def hf_model(self) -> str:
|
|
return self.args.hf_model
|
|
|
|
@property
|
|
def hf_temperature(self) -> float:
|
|
return self.args.hf_temperature
|
|
|
|
@property
|
|
def hf_api_key(self) -> Optional[str]:
|
|
return self.args.hf_api_key
|
|
|
|
# Mistral properties
|
|
@property
|
|
def mistral_enabled(self) -> bool:
|
|
return self.args.mistral_enable
|
|
|
|
@property
|
|
def mistral_model(self) -> str:
|
|
return self.args.mistral_model
|
|
|
|
@property
|
|
def mistral_temperature(self) -> float:
|
|
return self.args.mistral_temperature
|
|
|
|
@property
|
|
def mistral_token_limit(self) -> int:
|
|
return self.args.mistral_token_limit
|
|
|
|
@property
|
|
def mistral_api_key(self) -> Optional[str]:
|
|
return self.args.mistral_api_key
|
|
|
|
@property
|
|
def deepseek_enabled(self) -> bool:
|
|
return self.args.deepseek_enable
|
|
|
|
@property
|
|
def deepseek_model(self) -> str:
|
|
return self.args.deepseek_model
|
|
|
|
@property
|
|
def deepseek_temperature(self) -> float:
|
|
return self.args.deepseek_temperature
|
|
|
|
@property
|
|
def deepseek_token_limit(self) -> Optional[int]:
|
|
# Return None if 0 to use model default
|
|
return self.args.deepseek_token_limit if self.args.deepseek_token_limit > 0 else None
|
|
===== FILE: sia/delete_command.py =====
|
|
from .command import Command
|
|
from .command_result import CommandResult
|
|
from .working_memory import WorkingMemory
|
|
|
|
class DeleteCommand(Command):
|
|
"""
|
|
Command to delete an entry from working memory.
|
|
Ensures proper cleanup of entry resources before removal.
|
|
|
|
Attributes:
|
|
id: Unique identifier of entry to delete
|
|
"""
|
|
|
|
def __init__(self, id: str):
|
|
"""
|
|
Initialize delete command.
|
|
|
|
Args:
|
|
id: Unique identifier of entry to delete
|
|
"""
|
|
self.id = id
|
|
|
|
def execute(self, memory: WorkingMemory) -> CommandResult:
|
|
"""
|
|
Delete the specified entry from working memory.
|
|
Performs cleanup on the entry before removal.
|
|
|
|
Args:
|
|
memory: WorkingMemory instance to delete entry from
|
|
|
|
Returns:
|
|
CommandResult: Success if entry was found and deleted,
|
|
failure with message if entry not found
|
|
"""
|
|
entry = memory.get_entry(self.id)
|
|
if entry is None:
|
|
return CommandResult.failure(f"Entry with id '{self.id}' not found")
|
|
|
|
# Perform cleanup before removing
|
|
entry.cleanup()
|
|
memory.remove_entry(self.id)
|
|
return CommandResult.success()
|
|
===== FILE: sia/entry/__init__.py =====
|
|
from abc import ABC, abstractmethod
|
|
import xml.etree.ElementTree as ET
|
|
from typing import Callable, List
|
|
|
|
class Entry(ABC):
|
|
"""
|
|
Abstract base class for all entry types in the working memory.
|
|
Provides observable pattern functionality.
|
|
"""
|
|
|
|
def __init__(self, id: str):
|
|
"""
|
|
Initialize a new entry with provided id and timestamp.
|
|
|
|
Args:
|
|
id: Unique identifier for this entry
|
|
"""
|
|
self.id = id
|
|
self._change_handlers: List[Callable[['Entry'], None]] = []
|
|
|
|
def add_change_handler(self, handler: Callable[['Entry'], None]) -> None:
|
|
"""Add a callback for entry changes."""
|
|
if handler not in self._change_handlers:
|
|
self._change_handlers.append(handler)
|
|
|
|
def notify_change(self) -> None:
|
|
"""Notify all handlers of entry state change."""
|
|
for handler in self._change_handlers:
|
|
handler(self)
|
|
|
|
@abstractmethod
|
|
def update(self) -> None:
|
|
"""
|
|
Update the entry's state.
|
|
Must be implemented by concrete classes.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def generate_context(self) -> ET.Element:
|
|
"""
|
|
Generate an XML Element representing this entry's context.
|
|
Must be implemented by concrete classes.
|
|
|
|
Returns:
|
|
ET.Element: XML element containing the entry's data
|
|
"""
|
|
pass
|
|
|
|
def serialize(self) -> dict[str, str]:
|
|
"""
|
|
Collect all public attributes of the entry into a dictionary.
|
|
"""
|
|
pass
|
|
|
|
def cleanup(self) -> None:
|
|
"""
|
|
Clean up any resources used by this entry.
|
|
Should be overridden by classes that need cleanup.
|
|
Default implementation does nothing.
|
|
"""
|
|
pass
|
|
|
|
def reset(self) -> None:
|
|
"""
|
|
Reset the entry as if update was never called.
|
|
Default implementation does nothing.
|
|
"""
|
|
pass
|
|
===== FILE: sia/entry/background_entry.py =====
|
|
import subprocess
|
|
import xml.etree.ElementTree as ET
|
|
from typing import Optional
|
|
|
|
from . import Entry
|
|
|
|
class BackgroundEntry(Entry):
|
|
"""
|
|
Entry type for long-running background processes.
|
|
|
|
Attributes:
|
|
script: The script/command to execute
|
|
stdout: Captured standard output
|
|
stderr: Captured standard error
|
|
process: The running subprocess.Popen instance
|
|
exit_code: Exit code when process completes
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
id: str,
|
|
work_dir: str,
|
|
script: str,
|
|
):
|
|
"""
|
|
Initialize a new background entry.
|
|
|
|
Args:
|
|
id: Unique identifier for this entry
|
|
work_dir: Working directory for the process
|
|
script: The script/command to execute
|
|
"""
|
|
super().__init__(id)
|
|
self.work_dir = work_dir
|
|
self.script = script
|
|
self._process: Optional[subprocess.Popen] = None
|
|
self.stdout = ""
|
|
self.stderr = ""
|
|
self.exit_code = None
|
|
|
|
@property
|
|
def pid(self) -> Optional[int]:
|
|
"""Get the process ID (None if not running)."""
|
|
return self._process.pid if self._process is not None else None
|
|
|
|
def cleanup(self) -> None:
|
|
"""
|
|
Clean up the background process if it's still running.
|
|
Ensures process is terminated and file handles are closed.
|
|
"""
|
|
if self._process is not None:
|
|
try:
|
|
if self._process.stdout:
|
|
self._process.stdout.close()
|
|
if self._process.stderr:
|
|
self._process.stderr.close()
|
|
if self._process.poll() is None:
|
|
self._process.terminate()
|
|
try:
|
|
self._process.wait(timeout=1.0)
|
|
except subprocess.TimeoutExpired:
|
|
self._process.kill()
|
|
self._process.wait()
|
|
except:
|
|
pass # Ignore cleanup errors
|
|
finally:
|
|
self._process = None
|
|
|
|
def reset(self) -> None:
|
|
"""
|
|
Cleans up existing process and resets output buffers.
|
|
"""
|
|
self.cleanup()
|
|
self.stdout = ""
|
|
self.stderr = ""
|
|
self.exit_code = None
|
|
self.notify_change()
|
|
|
|
def update(self) -> None:
|
|
"""
|
|
Start the process if not running and collect any new output.
|
|
Updates stdout and stderr with any new output.
|
|
"""
|
|
if self._process is None and self.exit_code is None:
|
|
self._process = subprocess.Popen(
|
|
self.script,
|
|
cwd=self.work_dir,
|
|
shell=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
bufsize=1 # Line buffered
|
|
)
|
|
self.notify_change()
|
|
return
|
|
|
|
if self._process is None:
|
|
return
|
|
|
|
exit_code = self._process.poll()
|
|
if exit_code is not None:
|
|
try:
|
|
remaining_out, remaining_err = self._process.communicate(timeout=0.1)
|
|
self.stdout += remaining_out
|
|
self.stderr += remaining_err
|
|
except subprocess.TimeoutExpired:
|
|
pass # Process didn't finish communicating, try again next update
|
|
|
|
self.exit_code = exit_code
|
|
self.cleanup()
|
|
self.notify_change()
|
|
return
|
|
|
|
if self._process.stdout:
|
|
while True:
|
|
try:
|
|
line = self._process.stdout.readline()
|
|
if not line:
|
|
break
|
|
self.stdout += line
|
|
except:
|
|
break
|
|
|
|
if self._process.stderr:
|
|
while True:
|
|
try:
|
|
line = self._process.stderr.readline()
|
|
if not line:
|
|
break
|
|
self.stderr += line
|
|
except:
|
|
break
|
|
self.notify_change()
|
|
|
|
def generate_context(self) -> ET.Element:
|
|
"""
|
|
Generate an XML Element representing this background entry.
|
|
|
|
Returns:
|
|
ET.Element: XML element containing the entry's data
|
|
"""
|
|
element = ET.Element("background", {"id": self.id})
|
|
if self._process is not None:
|
|
element.set("pid", str(self._process.pid))
|
|
elif self.exit_code is not None:
|
|
element.set("exit_code", str(self.exit_code))
|
|
element.text = self.script
|
|
stdout_elem = ET.SubElement(element, "stdout")
|
|
stdout_elem.text = self.stdout
|
|
stderr_elem = ET.SubElement(element, "stderr")
|
|
stderr_elem.text = self.stderr
|
|
|
|
return element
|
|
|
|
def serialize(self):
|
|
"""
|
|
Collect all public attributes of the entry into a dictionary.
|
|
"""
|
|
return {
|
|
"type": "background",
|
|
"id": self.id,
|
|
"work_dir": str(self.work_dir),
|
|
"script": self.script,
|
|
"stdout": self.stdout,
|
|
"stderr": self.stderr,
|
|
"exit_code": self.exit_code,
|
|
}
|
|
===== FILE: sia/entry/entry_factory.py =====
|
|
from pathlib import Path
|
|
|
|
from . import Entry
|
|
from ..io_buffer import IOBuffer
|
|
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_entry import SingleEntry
|
|
from .write_entry import WriteEntry
|
|
|
|
class EntryFactory:
|
|
def create_entry(data: dict, work_dir: Path, io_buffer: IOBuffer) -> Entry:
|
|
entry_type = data.get("type")
|
|
entry_id = data.get("id")
|
|
|
|
if entry_type == "background":
|
|
return BackgroundEntry(entry_id, work_dir, data["script"])
|
|
|
|
elif entry_type == "read_stdin":
|
|
return ReadEntry(entry_id, io_buffer)
|
|
|
|
elif entry_type == "reasoning":
|
|
return ReasoningEntry(entry_id, data["content"])
|
|
|
|
elif entry_type == "repeat":
|
|
return RepeatEntry(
|
|
entry_id,
|
|
work_dir,
|
|
data["script"],
|
|
data.get("timeout"),
|
|
data.get("limit")
|
|
)
|
|
|
|
elif entry_type == "single":
|
|
return SingleEntry(
|
|
entry_id,
|
|
work_dir,
|
|
data["script"],
|
|
data.get("timeout"),
|
|
data.get("limit")
|
|
)
|
|
|
|
elif entry_type == "write":
|
|
return WriteEntry(entry_id, data["content"], io_buffer)
|
|
|
|
raise ValueError(f"Unknown entry type: {entry_type}")
|
|
|
|
def update_entry(entry: Entry, data: dict):
|
|
if isinstance(entry, Entry):
|
|
if "id" in data:
|
|
entry.id = data["id"]
|
|
|
|
if isinstance(entry, SingleEntry):
|
|
if "work_dir" in data:
|
|
entry.work_dir = Path(data["work_dir"])
|
|
if "script" in data:
|
|
entry.script = data["script"]
|
|
if "timeout" in data:
|
|
entry.timeout = float(data["timeout"]) if data["timeout"] is not None else None
|
|
if "limit" in data:
|
|
entry.limit = int(data["limit"]) if data["limit"] else None
|
|
if "stdout" in data:
|
|
entry.stdout = data["stdout"]
|
|
if "stderr" in data:
|
|
entry.stderr = data["stderr"]
|
|
if "exit_code" in data:
|
|
entry.exit_code = int(data["exit_code"]) if data["exit_code"] is not None else None
|
|
if "executed" in data:
|
|
entry.executed = bool(data["executed"])
|
|
if "timed_out" in data:
|
|
entry.timed_out = bool(data["timed_out"])
|
|
|
|
if isinstance(entry, RepeatEntry):
|
|
if "work_dir" in data:
|
|
entry.work_dir = Path(data["work_dir"])
|
|
if "script" in data:
|
|
entry.script = data["script"]
|
|
if "timeout" in data:
|
|
entry.timeout = float(data["timeout"]) if data["timeout"] is not None else None
|
|
if "limit" in data:
|
|
entry.limit = int(data["limit"]) if data["limit"] else None
|
|
if "stdout" in data:
|
|
entry.stdout = data["stdout"]
|
|
if "stderr" in data:
|
|
entry.stderr = data["stderr"]
|
|
if "exit_code" in data:
|
|
entry.exit_code = int(data["exit_code"]) if data["exit_code"] is not None else None
|
|
if "executed" in data:
|
|
entry.executed = bool(data["executed"])
|
|
if "timed_out" in data:
|
|
entry.timed_out = bool(data["timed_out"])
|
|
|
|
elif isinstance(entry, BackgroundEntry):
|
|
if "work_dir" in data:
|
|
entry.work_dir = Path(data["work_dir"])
|
|
if "script" in data:
|
|
entry.script = data["script"]
|
|
if "stdout" in data:
|
|
entry.stdout = data["stdout"]
|
|
if "stderr" in data:
|
|
entry.stderr = data["stderr"]
|
|
if "exit_code" in data:
|
|
entry.exit_code = int(data["exit_code"]) if data["exit_code"] is not None else None
|
|
|
|
elif isinstance(entry, ParseErrorEntry):
|
|
if "content" in data:
|
|
entry.content = data["content"]
|
|
if "error" in data:
|
|
entry.error = data["error"]
|
|
|
|
elif isinstance(entry, ReadEntry):
|
|
if "content" in data:
|
|
entry.content = data["content"]
|
|
if "read" in data:
|
|
entry.read = bool(data["read"])
|
|
|
|
elif isinstance(entry, ReasoningEntry):
|
|
if "content" in data:
|
|
entry.content = data["content"]
|
|
|
|
elif isinstance(entry, WriteEntry):
|
|
if "content" in data:
|
|
entry.content = data["content"]
|
|
if "written" in data:
|
|
entry.written = bool(data["written"])
|
|
===== FILE: sia/entry/parse_error_entry.py =====
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from . import Entry
|
|
|
|
class ParseErrorEntry(Entry):
|
|
"""
|
|
Entry type for parse and validation errors.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
id: str,
|
|
content: str,
|
|
error: str,
|
|
):
|
|
"""
|
|
Initialize a new parse error entry.
|
|
|
|
Args:
|
|
id: Unique identifier for this entry
|
|
content: Original content that failed to parse
|
|
error: Error message describing the failure
|
|
"""
|
|
super().__init__(id)
|
|
self.content = content
|
|
self.error = error
|
|
|
|
def update(self) -> None:
|
|
pass
|
|
|
|
def generate_context(self) -> ET.Element:
|
|
"""
|
|
Generate an XML Element representing this parse error entry.
|
|
|
|
Returns:
|
|
ET.Element: XML element containing the entry's data
|
|
"""
|
|
element = ET.Element("parse_error", {"id": self.id})
|
|
error_elem = ET.SubElement(element, "error")
|
|
error_elem.text = self.error
|
|
content_elem = ET.SubElement(element, "content")
|
|
content_elem.text = self.content
|
|
|
|
return element
|
|
|
|
def serialize(self):
|
|
"""
|
|
Collect all public attributes of the entry into a dictionary.
|
|
"""
|
|
return {
|
|
"type": "parse_error",
|
|
"id": self.id,
|
|
"content": self.content,
|
|
"error": self.error,
|
|
}
|
|
===== FILE: sia/entry/read_entry.py =====
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from . import Entry
|
|
from ..io_buffer import IOBuffer
|
|
|
|
class ReadEntry(Entry):
|
|
"""
|
|
Entry type for reading content from standard input.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
id: str,
|
|
io_buffer: IOBuffer,
|
|
):
|
|
"""
|
|
Initialize a new read entry.
|
|
|
|
Args:
|
|
id: Unique identifier for this entry
|
|
io_buffer: Buffer to use for IO operations
|
|
"""
|
|
super().__init__(id)
|
|
self.content: str = ""
|
|
self.read = False
|
|
self._io_buffer = io_buffer
|
|
|
|
def update(self) -> None:
|
|
"""
|
|
Read from stdin if not already read.
|
|
Uses the provided IO buffer for the actual read operation.
|
|
"""
|
|
if not self.read:
|
|
self.content = self._io_buffer.read()
|
|
self.read = True
|
|
self.notify_change()
|
|
|
|
def reset(self) -> None:
|
|
"""
|
|
Reset the entry state to its initial state.
|
|
"""
|
|
self.read = False
|
|
self.content = ""
|
|
self.notify_change()
|
|
|
|
def generate_context(self) -> ET.Element:
|
|
"""
|
|
Generate an XML Element representing this read entry.
|
|
|
|
Returns:
|
|
ET.Element: XML element containing the entry's data
|
|
"""
|
|
element = ET.Element("read_stdin", {"id": self.id})
|
|
if self.read:
|
|
element.text = self.content
|
|
|
|
return element
|
|
|
|
def serialize(self):
|
|
"""
|
|
Collect all public attributes of the entry into a dictionary.
|
|
"""
|
|
return {
|
|
"type": "read_stdin",
|
|
"id": self.id,
|
|
"content": self.content,
|
|
"read": self.read,
|
|
}
|
|
===== FILE: sia/entry/reasoning_entry.py =====
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from . import Entry
|
|
|
|
class ReasoningEntry(Entry):
|
|
"""
|
|
Entry type for agent reasoning steps.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
id: str,
|
|
content: str,
|
|
):
|
|
"""
|
|
Initialize a new reasoning entry.
|
|
|
|
Args:
|
|
id: Unique identifier for this entry
|
|
timestamp: Creation timestamp for this entry
|
|
content: The reasoning text
|
|
"""
|
|
super().__init__(id)
|
|
self.content = content
|
|
|
|
def update(self) -> None:
|
|
"""No update needed for reasoning entries."""
|
|
pass
|
|
|
|
def generate_context(self) -> ET.Element:
|
|
"""
|
|
Generate an XML Element representing this reasoning entry.
|
|
|
|
Returns:
|
|
ET.Element: XML element containing the entry's data
|
|
"""
|
|
element = ET.Element("reasoning", {"id": self.id})
|
|
element.text = self.content
|
|
|
|
return element
|
|
|
|
def serialize(self):
|
|
"""
|
|
Collect all public attributes of the entry into a dictionary.
|
|
"""
|
|
return {
|
|
"type": "reasoning",
|
|
"id": self.id,
|
|
"content": self.content,
|
|
}
|
|
===== FILE: sia/entry/repeat_entry.py =====
|
|
from pathlib import Path
|
|
import subprocess
|
|
import xml.etree.ElementTree as ET
|
|
from typing import Optional
|
|
|
|
from . import Entry
|
|
|
|
class RepeatEntry(Entry):
|
|
"""
|
|
Entry type for scripts that are executed on every update.
|
|
"""
|
|
|
|
default_timeout = 1
|
|
default_limit = 1024
|
|
|
|
def __init__(
|
|
self,
|
|
id: str,
|
|
work_dir: Path,
|
|
script: str,
|
|
timeout: Optional[float] = None,
|
|
limit: Optional[int] = None,
|
|
):
|
|
"""
|
|
Initialize a new repeat entry.
|
|
|
|
Args:
|
|
id: Unique identifier for this entry
|
|
script: The script/command to execute
|
|
timeout: Maximum time to wait for script execution
|
|
limit: Maximum number of characters to capture from stdout/stderr
|
|
"""
|
|
super().__init__(id)
|
|
self.work_dir = work_dir
|
|
self.script = script
|
|
self.timeout = timeout
|
|
self.limit = limit
|
|
self.stdout: str = ""
|
|
self.stderr: str = ""
|
|
self.exit_code: Optional[int] = None
|
|
self.timed_out: bool = False
|
|
|
|
def update(self) -> None:
|
|
"""
|
|
Execute the script and update the output.
|
|
Captures stdout, stderr and exit code from each execution.
|
|
"""
|
|
try:
|
|
process = subprocess.run(
|
|
self.script,
|
|
cwd=self.work_dir,
|
|
timeout=(self.timeout or self.default_timeout),
|
|
shell=True,
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
self.stdout = process.stdout
|
|
self.stderr = process.stderr
|
|
self.exit_code = process.returncode
|
|
self.timed_out = False
|
|
except subprocess.TimeoutExpired as e:
|
|
self.timed_out = True
|
|
self.notify_change()
|
|
|
|
def generate_context(self) -> ET.Element:
|
|
"""
|
|
Generate an XML Element representing this repeat entry.
|
|
|
|
Returns:
|
|
ET.Element: XML element containing the entry's data
|
|
"""
|
|
element = ET.Element("repeat", {"id": self.id})
|
|
if self.timeout:
|
|
element.set("timeout", str(self.timeout))
|
|
element.text = self.script
|
|
if self.timed_out:
|
|
element.set("timed_out", "true")
|
|
elif self.exit_code is not None:
|
|
element.set("exit_code", str(self.exit_code))
|
|
if self.limit:
|
|
element.set("limit", str(self.limit))
|
|
if len(self.stdout) > (self.limit or self.default_limit):
|
|
element.set("stdout_truncated", "true")
|
|
element.set("stdout_length", str(len(self.stdout)))
|
|
stdout_elem = ET.SubElement(element, "stdout")
|
|
stdout_elem.text = self.stdout[:(self.limit or self.default_limit)]
|
|
if len(self.stderr) > (self.limit or self.default_limit):
|
|
element.set("stderr_truncated", "true")
|
|
element.set("stderr_length", str(len(self.stderr)))
|
|
stderr_elem = ET.SubElement(element, "stderr")
|
|
stderr_elem.text = self.stderr[:(self.limit or self.default_limit)]
|
|
|
|
return element
|
|
|
|
def serialize(self):
|
|
"""
|
|
Collect all public attributes of the entry into a dictionary.
|
|
"""
|
|
return {
|
|
"type": "repeat",
|
|
"id": self.id,
|
|
"work_dir": str(self.work_dir),
|
|
"script": self.script,
|
|
"timeout": self.timeout,
|
|
"limit": self.limit,
|
|
"stdout": self.stdout,
|
|
"stderr": self.stderr,
|
|
"exit_code": self.exit_code,
|
|
"timed_out": self.timed_out
|
|
}
|
|
|
|
===== FILE: sia/entry/single_entry.py =====
|
|
from pathlib import Path
|
|
import subprocess
|
|
import xml.etree.ElementTree as ET
|
|
from typing import Optional
|
|
|
|
from . import Entry
|
|
|
|
class SingleEntry(Entry):
|
|
"""
|
|
Entry type for one-time script executions.
|
|
"""
|
|
|
|
default_timeout = 1
|
|
default_limit = 1024
|
|
|
|
def __init__(
|
|
self,
|
|
id: str,
|
|
work_dir: Path,
|
|
script: str,
|
|
timeout: Optional[float] = None,
|
|
limit: Optional[int] = None,
|
|
):
|
|
"""
|
|
Initialize a new single shot entry.
|
|
|
|
Args:
|
|
id: Unique identifier for this entry
|
|
script: The script/command to execute
|
|
timeout: Maximum time to wait for script execution
|
|
limit: Maximum number of characters to capture from stdout/stderr
|
|
"""
|
|
super().__init__(id)
|
|
self.work_dir = work_dir
|
|
self.script = script
|
|
self.timeout = timeout
|
|
self.limit = limit
|
|
self.stdout: str = ""
|
|
self.stderr: str = ""
|
|
self.exit_code: Optional[int] = None
|
|
self.executed: bool = False
|
|
self.timed_out: bool = False
|
|
|
|
def reset(self) -> None:
|
|
"""Reset execution state to allow running again."""
|
|
self.executed = False
|
|
self.timed_out = False
|
|
self.stdout = ""
|
|
self.stderr = ""
|
|
self.exit_code = None
|
|
self.notify_change()
|
|
|
|
def update(self) -> None:
|
|
"""
|
|
Execute the script if not already executed.
|
|
Captures stdout, stderr and exit code.
|
|
"""
|
|
if self.executed:
|
|
return
|
|
self.executed = True
|
|
try:
|
|
process = subprocess.run(
|
|
self.script,
|
|
cwd=self.work_dir,
|
|
timeout=(self.timeout or self.default_timeout),
|
|
shell=True,
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
self.stdout = process.stdout
|
|
self.stderr = process.stderr
|
|
self.exit_code = process.returncode
|
|
except subprocess.TimeoutExpired as e:
|
|
self.timed_out = True
|
|
self.notify_change()
|
|
|
|
def generate_context(self) -> ET.Element:
|
|
"""
|
|
Generate an XML Element representing this single shot entry.
|
|
|
|
Returns:
|
|
ET.Element: XML element containing the entry's data
|
|
"""
|
|
element = ET.Element("single", {"id": self.id})
|
|
if self.timeout:
|
|
element.set("timeout", str(self.timeout))
|
|
element.text = self.script
|
|
if self.timed_out:
|
|
element.set("timed_out", "true")
|
|
elif self.executed:
|
|
element.set("exit_code", str(self.exit_code))
|
|
if self.limit:
|
|
element.set("limit", str(self.limit))
|
|
if len(self.stdout) > (self.limit or self.default_limit):
|
|
element.set("stdout_truncated", "true")
|
|
element.set("stdout_length", str(len(self.stdout)))
|
|
stdout_elem = ET.SubElement(element, "stdout")
|
|
stdout_elem.text = self.stdout[:(self.limit or self.default_limit)]
|
|
if len(self.stderr) > (self.limit or self.default_limit):
|
|
element.set("stderr_truncated", "true")
|
|
element.set("stderr_length", str(len(self.stderr)))
|
|
stderr_elem = ET.SubElement(element, "stderr")
|
|
stderr_elem.text = self.stderr[:(self.limit or self.default_limit)]
|
|
return element
|
|
|
|
def serialize(self):
|
|
"""
|
|
Collect all public attributes of the entry into a dictionary.
|
|
"""
|
|
return {
|
|
"type": "single",
|
|
"id": self.id,
|
|
"work_dir": str(self.work_dir),
|
|
"script": self.script,
|
|
"timeout": self.timeout,
|
|
"limit": self.limit,
|
|
"stdout": self.stdout,
|
|
"stderr": self.stderr,
|
|
"exit_code": self.exit_code,
|
|
"executed": self.executed,
|
|
"timed_out": self.timed_out
|
|
}
|
|
===== FILE: sia/entry/write_entry.py =====
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from . import Entry
|
|
from ..io_buffer import IOBuffer
|
|
|
|
class WriteEntry(Entry):
|
|
"""
|
|
Entry type for writing content to standard output.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
id: str,
|
|
content: str,
|
|
io_buffer: IOBuffer,
|
|
):
|
|
"""
|
|
Initialize a new write entry.
|
|
|
|
Args:
|
|
id: Unique identifier for this entry
|
|
content: Text to write to stdout
|
|
io_buffer: Buffer to use for IO operations
|
|
"""
|
|
super().__init__(id)
|
|
self.content = content
|
|
self.written: bool = False
|
|
self._io_buffer = io_buffer
|
|
|
|
def update(self) -> None:
|
|
"""
|
|
Write the content to stdout if not already written.
|
|
Uses the provided IO buffer for the actual write operation.
|
|
"""
|
|
if not self.written:
|
|
self._io_buffer.write(self.content)
|
|
self.written = True
|
|
self.notify_change()
|
|
|
|
def reset(self) -> None:
|
|
"""
|
|
Reset the entry state to its initial state.
|
|
"""
|
|
self.written = False
|
|
self.notify_change()
|
|
|
|
def generate_context(self) -> ET.Element:
|
|
"""
|
|
Generate an XML Element representing this write entry.
|
|
|
|
Returns:
|
|
ET.Element: XML element containing the entry's data
|
|
"""
|
|
element = ET.Element("write_stdout", {"id": self.id})
|
|
element.text = self.content
|
|
return element
|
|
|
|
def serialize(self):
|
|
"""
|
|
Collect all public attributes of the entry into a dictionary.
|
|
"""
|
|
return {
|
|
"type": "write",
|
|
"id": self.id,
|
|
"content": self.content,
|
|
"written": self.written,
|
|
}
|
|
|
|
===== FILE: sia/io_buffer.py =====
|
|
from abc import ABC, abstractmethod
|
|
|
|
class IOBuffer(ABC):
|
|
"""
|
|
Abstract base class defining the interface for input/output operations.
|
|
|
|
This interface allows for different implementations of IO handling,
|
|
such as direct system IO or buffered web interface communication.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def read(self) -> str:
|
|
"""
|
|
Read and return available input.
|
|
|
|
Should clear the input buffer after reading.
|
|
|
|
Returns:
|
|
str: Content from input buffer, or empty string if no input available
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def write(self, content: str) -> None:
|
|
"""
|
|
Write content to output.
|
|
|
|
Args:
|
|
content: String content to write
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def buffer_length(self) -> int:
|
|
"""
|
|
Get the current length of buffered input.
|
|
|
|
Returns:
|
|
int: Number of characters in the input buffer
|
|
"""
|
|
pass
|
|
===== FILE: sia/iteration_logger.py =====
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
import xml.etree.ElementTree as ET
|
|
import hashlib
|
|
|
|
from .util import format_timestamp
|
|
|
|
class IterationLogger:
|
|
"""Logs agent iterations to XML files"""
|
|
|
|
def __init__(
|
|
self,
|
|
iterations_dir: Path,
|
|
system_prompt: str,
|
|
action_schema: str,
|
|
):
|
|
"""Initialize with directory for storing iteration files"""
|
|
self.iterations_dir = iterations_dir
|
|
self.iterations_dir.mkdir(parents=True, exist_ok=True)
|
|
self._system_prompt_hash = hashlib.sha256(system_prompt.encode()).hexdigest()
|
|
self._action_schema_hash = hashlib.sha256(action_schema.encode()).hexdigest()
|
|
|
|
def log_iteration(
|
|
self,
|
|
timestamp: datetime,
|
|
context: str,
|
|
response: str,
|
|
):
|
|
"""
|
|
Save an iteration to an XML file
|
|
|
|
Args:
|
|
context: The context as ElementTree
|
|
response: Raw response from LLM
|
|
"""
|
|
filename = f"iteration_{format_timestamp(timestamp)}.xml"
|
|
filepath = self.iterations_dir / filename
|
|
|
|
root = ET.Element("iteration")
|
|
|
|
root.set("system_prompt_hash", self._system_prompt_hash)
|
|
root.set("action_schema_hash", self._action_schema_hash)
|
|
|
|
context_elem = ET.SubElement(root, "context")
|
|
context_elem.text = context
|
|
|
|
response_elem = ET.SubElement(root, "response")
|
|
response_elem.text = response
|
|
|
|
tree = ET.ElementTree(root)
|
|
tree.write(filepath, encoding="utf-8", xml_declaration=True)
|
|
===== FILE: sia/iteration_parser.py =====
|
|
from pathlib import Path
|
|
from typing import List
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from .entry import Entry
|
|
from .entry.background_entry import BackgroundEntry
|
|
from .entry.parse_error_entry import ParseErrorEntry
|
|
from .entry.read_entry import ReadEntry
|
|
from .entry.reasoning_entry import ReasoningEntry
|
|
from .entry.repeat_entry import RepeatEntry
|
|
from .entry.single_entry import SingleEntry
|
|
from .entry.write_entry import WriteEntry
|
|
|
|
class IterationParser:
|
|
"""Parses iteration XML files into entries"""
|
|
|
|
@staticmethod
|
|
def parse_iteration(content: str, work_dir: Path, io_buffer) -> tuple[str, str, List[Entry]]:
|
|
"""
|
|
Parse iteration XML content into context, response and entries
|
|
|
|
Args:
|
|
content: Raw XML content as string
|
|
io_buffer: Buffer to use for IO operations
|
|
|
|
Returns:
|
|
Tuple of (context_str, response_str, entry_list)
|
|
"""
|
|
root = ET.fromstring(content)
|
|
context_elem = root.find("context")
|
|
|
|
if context_elem is None:
|
|
raise ValueError("Invalid iteration file - missing context")
|
|
|
|
context = ET.fromstring(context_elem.text)
|
|
|
|
entries = []
|
|
for elem in context:
|
|
if elem.tag == "background":
|
|
entries.append(IterationParser._parse_background(elem, work_dir))
|
|
elif elem.tag == "parse_error":
|
|
entries.append(IterationParser._parse_parse_error(elem))
|
|
elif elem.tag == "read_stdin":
|
|
entries.append(IterationParser._parse_read(elem, io_buffer))
|
|
elif elem.tag == "reasoning":
|
|
entries.append(IterationParser._parse_reasoning(elem))
|
|
elif elem.tag == "repeat":
|
|
entries.append(IterationParser._parse_repeat(elem, work_dir))
|
|
elif elem.tag == "single":
|
|
entries.append(IterationParser._parse_single(elem, work_dir))
|
|
elif elem.tag == "write_stdout":
|
|
entries.append(IterationParser._parse_write(elem, io_buffer))
|
|
|
|
return entries
|
|
|
|
@staticmethod
|
|
def _parse_background(elem: ET.Element, work_dir: Path) -> BackgroundEntry:
|
|
entry = BackgroundEntry(
|
|
id=elem.get("id"),
|
|
script=elem.text,
|
|
work_dir=work_dir
|
|
)
|
|
if elem.get("exit_code"):
|
|
entry.exit_code = int(elem.get("exit_code"))
|
|
stdout = elem.find("stdout")
|
|
if stdout is not None:
|
|
entry.stdout = stdout.text or ""
|
|
stderr = elem.find("stderr")
|
|
if stderr is not None:
|
|
entry.stderr = stderr.text or ""
|
|
return entry
|
|
|
|
@staticmethod
|
|
def _parse_parse_error(elem: ET.Element) -> ParseErrorEntry:
|
|
error = elem.find("error")
|
|
content = elem.find("content")
|
|
return ParseErrorEntry(
|
|
id=elem.get("id"),
|
|
content=content.text if content is not None else "",
|
|
error=error.text if error is not None else ""
|
|
)
|
|
|
|
@staticmethod
|
|
def _parse_read(elem: ET.Element, io_buffer) -> ReadEntry:
|
|
entry = ReadEntry(
|
|
id=elem.get("id"),
|
|
io_buffer=io_buffer
|
|
)
|
|
entry.content = elem.text or ""
|
|
entry.read = True
|
|
return entry
|
|
|
|
@staticmethod
|
|
def _parse_reasoning(elem: ET.Element) -> ReasoningEntry:
|
|
return ReasoningEntry(
|
|
id=elem.get("id"),
|
|
content=elem.text or ""
|
|
)
|
|
|
|
@staticmethod
|
|
def _parse_repeat(elem: ET.Element, work_dir: Path) -> RepeatEntry:
|
|
entry = RepeatEntry(
|
|
id=elem.get("id"),
|
|
work_dir=work_dir,
|
|
script=elem.text,
|
|
timeout=float(elem.get("timeout")) if elem.get("timeout") else None,
|
|
limit=int(elem.get("limit")) if elem.get("limit") else None
|
|
)
|
|
if elem.get("exit_code"):
|
|
entry.exit_code = int(elem.get("exit_code"))
|
|
entry.timed_out = elem.get("timed_out") == "true"
|
|
stdout = elem.find("stdout")
|
|
if stdout is not None:
|
|
entry.stdout = stdout.text or ""
|
|
stderr = elem.find("stderr")
|
|
if stderr is not None:
|
|
entry.stderr = stderr.text or ""
|
|
return entry
|
|
|
|
@staticmethod
|
|
def _parse_single(elem: ET.Element, work_dir: Path) -> SingleEntry:
|
|
entry = SingleEntry(
|
|
id=elem.get("id"),
|
|
work_dir=work_dir,
|
|
script=elem.text,
|
|
timeout=float(elem.get("timeout")) if elem.get("timeout") else None,
|
|
limit=int(elem.get("limit")) if elem.get("limit") else None
|
|
)
|
|
if elem.get("exit_code"):
|
|
entry.exit_code = int(elem.get("exit_code"))
|
|
entry.executed = True
|
|
entry.timed_out = elem.get("timed_out") == "true"
|
|
stdout = elem.find("stdout")
|
|
if stdout is not None:
|
|
entry.stdout = stdout.text or ""
|
|
stderr = elem.find("stderr")
|
|
if stderr is not None:
|
|
entry.stderr = stderr.text or ""
|
|
return entry
|
|
|
|
@staticmethod
|
|
def _parse_write(elem: ET.Element, io_buffer) -> WriteEntry:
|
|
entry = WriteEntry(
|
|
id=elem.get("id"),
|
|
content=elem.text or "",
|
|
io_buffer=io_buffer
|
|
)
|
|
entry.written = True
|
|
return entry
|
|
===== FILE: sia/llm_engine/__init__.py =====
|
|
from typing import Callable, Iterator
|
|
from abc import ABC, abstractmethod
|
|
|
|
class LlmEngine(ABC):
|
|
@abstractmethod
|
|
def infer(self, system_prompt: str, main_context: str, should_stop: Callable[[], bool] = lambda: False) -> Iterator[str]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def token_count(self, system_prompt: str, main_context: str) -> int:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def token_limit(self) -> int:
|
|
pass
|
|
===== FILE: sia/llm_engine/deepseek_llm_engine.py =====
|
|
from typing import Callable, Iterator, Optional
|
|
import torch
|
|
from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer
|
|
from threading import Thread
|
|
from pathlib import Path
|
|
|
|
from . import LlmEngine
|
|
from .. import util
|
|
|
|
class DeepSeekLlmEngine(LlmEngine):
|
|
"""
|
|
LLM Engine implementation for DeepSeek models.
|
|
Supports fine-tuned DeepSeek-R1 and its distilled versions.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
model_path: str,
|
|
temperature: float = 0.6,
|
|
token_limit: Optional[int] = None,
|
|
api_key: Optional[str] = None,
|
|
):
|
|
"""
|
|
Initialize the DeepSeek LLM Engine.
|
|
|
|
Args:
|
|
model_path: Local path to the fine-tuned model
|
|
temperature: Sampling temperature (0.6 default as recommended)
|
|
token_limit: Maximum tokens to generate or context length override
|
|
api_key: HuggingFace API token if needed
|
|
"""
|
|
self._model_path = Path(model_path)
|
|
self._temperature = temperature
|
|
self._token_limit = token_limit
|
|
|
|
# Load tokenizer with trust_remote_code for DeepSeek models
|
|
self._tokenizer = AutoTokenizer.from_pretrained(
|
|
self._model_path,
|
|
token=api_key,
|
|
trust_remote_code=True,
|
|
)
|
|
|
|
# Set padding token to avoid warnings
|
|
if self._tokenizer.pad_token is None:
|
|
self._tokenizer.pad_token = self._tokenizer.eos_token
|
|
|
|
# Load model with 4-bit quantization by default
|
|
self._device_map = "auto"
|
|
|
|
self._model = AutoModelForCausalLM.from_pretrained(
|
|
self._model_path,
|
|
return_dict=True,
|
|
low_cpu_mem_usage=True,
|
|
trust_remote_code=True,
|
|
device_map=self._device_map,
|
|
load_in_4bit=True,
|
|
torch_dtype=torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16,
|
|
token=api_key,
|
|
)
|
|
|
|
# Ensure model is in evaluation mode
|
|
self._model.eval()
|
|
|
|
def infer(self, system_prompt: str, main_context: str, should_stop: Callable[[], bool] = lambda: False) -> Iterator[str]:
|
|
"""
|
|
Run inference using the system prompt and main context.
|
|
|
|
Args:
|
|
system_prompt: The system prompt string
|
|
main_context: The main context string after templating
|
|
should_stop: Callback that returns True when inference should stop
|
|
|
|
Returns:
|
|
Iterator[str]: An iterator that yields the generated text.
|
|
"""
|
|
# Tokenize input
|
|
inputs = self._tokenizer(system_prompt + "\n\n" + main_context, return_tensors="pt").to(self._device_map)
|
|
|
|
# Create streamer for token-by-token generation
|
|
streamer = TextIteratorStreamer(
|
|
self._tokenizer,
|
|
skip_prompt=True,
|
|
timeout=15.0
|
|
)
|
|
|
|
# Generate in a separate thread to enable streaming
|
|
generation_kwargs = {
|
|
"input_ids": inputs.input_ids,
|
|
"attention_mask": inputs.attention_mask,
|
|
"max_new_tokens": self.token_limit() if self._token_limit else 2048,
|
|
"temperature": self._temperature,
|
|
"do_sample": True,
|
|
"streamer": streamer,
|
|
"repetition_penalty": 1.1,
|
|
"pad_token_id": self._tokenizer.pad_token_id,
|
|
}
|
|
|
|
generation_thread = Thread(target=self._model.generate, kwargs=generation_kwargs)
|
|
generation_thread.start()
|
|
|
|
# Yield tokens as they become available
|
|
try:
|
|
for text in streamer:
|
|
yield text
|
|
if should_stop():
|
|
break
|
|
finally:
|
|
# Ensure thread is properly joined even if iteration is interrupted
|
|
generation_thread.join()
|
|
|
|
def token_count(self, system_prompt: str, main_context: str) -> int:
|
|
"""
|
|
Count tokens for the given system prompt and main context.
|
|
|
|
Args:
|
|
system_prompt: The system prompt string
|
|
main_context: The main context string
|
|
|
|
Returns:
|
|
int: Total number of tokens
|
|
"""
|
|
combined_prompt = f"{system_prompt}\n\n{main_context}"
|
|
return len(self._tokenizer.encode(combined_prompt))
|
|
|
|
def token_limit(self) -> int:
|
|
"""
|
|
Get the model's context window size.
|
|
|
|
Returns:
|
|
int: Maximum number of tokens the model can process
|
|
"""
|
|
if self._token_limit is not None:
|
|
return self._token_limit
|
|
|
|
# Try to detect model size from config
|
|
try:
|
|
config_file = self._model_path / "config.json"
|
|
if config_file.exists():
|
|
import json
|
|
with open(config_file, 'r') as f:
|
|
config = json.load(f)
|
|
if 'max_position_embeddings' in config:
|
|
return config['max_position_embeddings']
|
|
if 'model_max_length' in config:
|
|
return config['model_max_length']
|
|
except Exception:
|
|
pass
|
|
|
|
# Default to 8k if we can't determine
|
|
return 8192
|
|
===== FILE: sia/llm_engine/hf_llm_engine.py =====
|
|
from huggingface_hub import InferenceClient
|
|
from transformers import AutoTokenizer, AutoConfig
|
|
from typing import Iterator, Optional, Callable
|
|
|
|
from . import LlmEngine
|
|
|
|
class HfLlmEngine(LlmEngine):
|
|
"""
|
|
LLM Engine implementation using HuggingFace's InferenceClient.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
model: str,
|
|
temperature: float,
|
|
api_token: Optional[str],
|
|
):
|
|
"""
|
|
Initialize the HuggingFace Inference API LLM Engine.
|
|
|
|
Args:
|
|
model: HuggingFace model ID to use
|
|
temperature: Sampling temperature
|
|
api_token: HuggingFace API token
|
|
"""
|
|
self._model = model
|
|
self._temperature = temperature
|
|
|
|
self._tokenizer = AutoTokenizer.from_pretrained(model, token=api_token)
|
|
self._config = AutoConfig.from_pretrained(model, token=api_token)
|
|
self._client = InferenceClient(token=api_token)
|
|
|
|
def infer(self, system_prompt: str, main_context: str, should_stop: Callable[[], bool] = lambda: False) -> Iterator[str]:
|
|
"""
|
|
Run inference using the system prompt and main context.
|
|
|
|
Args:
|
|
system_prompt: The system prompt string
|
|
main_context: The main context string after templating
|
|
should_stop: Callback that returns True when inference should stop
|
|
|
|
Returns:
|
|
Iterator[str]: An iterator that yields the generated text.
|
|
"""
|
|
messages = [
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": main_context}
|
|
]
|
|
|
|
stream = self._client.chat_completion(
|
|
model=self._model,
|
|
messages=messages,
|
|
temperature=self._temperature,
|
|
stream=True
|
|
)
|
|
|
|
try:
|
|
for response in stream:
|
|
if should_stop():
|
|
stream.close()
|
|
break
|
|
if content := response.choices[0].delta.content:
|
|
yield content
|
|
finally:
|
|
stream.close()
|
|
|
|
def token_count(self, system_prompt: str, main_context: str) -> int:
|
|
messages = [
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": main_context}
|
|
]
|
|
prompt = self._tokenizer.apply_chat_template(
|
|
messages, tokenize=False, add_generation_prompt=True
|
|
)
|
|
return len(self._tokenizer.encode(prompt))
|
|
|
|
def token_limit(self) -> int:
|
|
"""
|
|
Get the model's context window size.
|
|
|
|
Returns:
|
|
int: Maximum number of tokens the model can process
|
|
"""
|
|
return self._config.max_position_embeddings
|
|
|
|
===== FILE: sia/llm_engine/local_llm_engine.py =====
|
|
from threading import Thread
|
|
from typing import Iterator, Optional, Callable
|
|
|
|
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline, TextIteratorStreamer
|
|
import torch
|
|
|
|
from . import LlmEngine
|
|
from .. import util
|
|
|
|
class LocalLlmEngine(LlmEngine):
|
|
def __init__(
|
|
self,
|
|
model_path: str,
|
|
temperature: float,
|
|
token_limit: int,
|
|
api_token: Optional[str],
|
|
):
|
|
"""
|
|
Initialize the LLM Engine with a model path.
|
|
|
|
Args:
|
|
model_path: Path to the model weights to be used.
|
|
temperature: Temperature for sampling
|
|
api_token: Huggingface API key
|
|
token_limit: Maximum number of tokens to generate
|
|
"""
|
|
self._temperature = temperature
|
|
self._token_limit = token_limit
|
|
self._tokenizer = AutoTokenizer.from_pretrained(model_path, token=api_token)
|
|
model = AutoModelForCausalLM.from_pretrained(
|
|
model_path,
|
|
return_dict=True,
|
|
low_cpu_mem_usage=True,
|
|
torch_dtype=torch.bfloat16,
|
|
device_map="auto",
|
|
trust_remote_code=True,
|
|
token=api_token,
|
|
)
|
|
if self._tokenizer.pad_token_id is None:
|
|
self._tokenizer.pad_token_id = self._tokenizer.eos_token_id
|
|
if model.config.pad_token_id is None:
|
|
model.config.pad_token_id = model.config.eos_token_id
|
|
self._pipeline = pipeline(
|
|
"text-generation",
|
|
model=model,
|
|
tokenizer=self._tokenizer,
|
|
torch_dtype=torch.bfloat16,
|
|
device_map="auto",
|
|
return_full_text=False,
|
|
)
|
|
|
|
def infer(self, system_prompt: str, main_context: str, should_stop: Callable[[], bool] = lambda: False) -> Iterator[str]:
|
|
"""
|
|
Run inference using the system prompt and main context.
|
|
|
|
Args:
|
|
system_prompt: The system prompt string
|
|
main_context: The main context string after templating
|
|
should_stop: Callback that returns True when inference should stop
|
|
|
|
Returns:
|
|
Iterator[str]: An iterator that yields the generated text.
|
|
"""
|
|
messages = [
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": main_context}
|
|
]
|
|
prompt = self._tokenizer.apply_chat_template(
|
|
messages, tokenize=False, add_generation_prompt=True
|
|
)
|
|
streamer = TextIteratorStreamer(
|
|
self._tokenizer,
|
|
skip_prompt=True
|
|
)
|
|
generation_thread = Thread(target=self._pipeline, kwargs=dict(
|
|
text_inputs=prompt,
|
|
do_sample=True,
|
|
temperature=self._temperature,
|
|
max_new_tokens=self.token_limit(),
|
|
streamer=streamer
|
|
))
|
|
generation_thread.start()
|
|
|
|
for text in util.stop_before_value(streamer, '<|eot_id|>'):
|
|
yield text
|
|
if should_stop():
|
|
break
|
|
|
|
generation_thread.join()
|
|
|
|
def token_count(self, system_prompt: str, main_context: str) -> int:
|
|
"""
|
|
Count tokens for the given system prompt and main context.
|
|
|
|
Args:
|
|
system_prompt: The system prompt string
|
|
main_context: The main context string
|
|
|
|
Returns:
|
|
int: Total number of tokens
|
|
"""
|
|
messages = [
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": main_context}
|
|
]
|
|
prompt = self._tokenizer.apply_chat_template(
|
|
messages, tokenize=False, add_generation_prompt=True
|
|
)
|
|
return len(self._tokenizer.encode(prompt))
|
|
|
|
def token_limit(self) -> int:
|
|
"""
|
|
Get the model's context window size.
|
|
|
|
Returns:
|
|
int: Maximum number of tokens the model can process
|
|
"""
|
|
if self._token_limit is not None:
|
|
return self._token_limit
|
|
else:
|
|
return self._pipeline.model.config.max_position_embeddings
|
|
|
|
===== FILE: sia/llm_engine/mistral_llm_engine.py =====
|
|
from typing import Iterator, Optional, Callable
|
|
from mistralai import Mistral
|
|
from mistral_common.protocol.instruct.messages import SystemMessage, UserMessage
|
|
from mistral_common.protocol.instruct.request import ChatCompletionRequest
|
|
from mistral_common.tokens.tokenizers.mistral import MistralTokenizer
|
|
|
|
from . import LlmEngine
|
|
|
|
class MistralLlmEngine(LlmEngine):
|
|
def __init__(
|
|
self,
|
|
model: str,
|
|
temperature: float,
|
|
token_limit: int,
|
|
api_key: str,
|
|
):
|
|
self._model = model
|
|
self._temperature = temperature
|
|
self._token_limit = token_limit
|
|
self._api_key = api_key
|
|
self._client = Mistral(api_key=api_key)
|
|
self._tokenizer = MistralTokenizer.v3()
|
|
|
|
def infer(self, system_prompt: str, main_context: str, should_stop: Callable[[], bool] = lambda: False) -> Iterator[str]:
|
|
messages = [
|
|
{
|
|
"role": "system",
|
|
"content": system_prompt,
|
|
},
|
|
{
|
|
"role": "user",
|
|
"content": main_context,
|
|
},
|
|
{
|
|
"role": "assistant",
|
|
"content": "<",
|
|
"prefix": True,
|
|
},
|
|
]
|
|
stream_response = self._client.chat.stream(
|
|
model=self._model,
|
|
messages=messages,
|
|
temperature=self._temperature,
|
|
)
|
|
|
|
try:
|
|
for chunk in stream_response:
|
|
if should_stop():
|
|
stream_response.response.close()
|
|
break
|
|
if content := chunk.data.choices[0].delta.content:
|
|
yield content
|
|
finally:
|
|
stream_response.response.close()
|
|
|
|
def token_count(self, system_prompt: str, main_context: str) -> int:
|
|
messages = [
|
|
SystemMessage(content=system_prompt),
|
|
UserMessage(content=main_context),
|
|
]
|
|
|
|
tokenized = self._tokenizer.encode_chat_completion(
|
|
ChatCompletionRequest(
|
|
messages=messages,
|
|
model=self._model
|
|
)
|
|
)
|
|
return len(tokenized.tokens)
|
|
|
|
def token_limit(self) -> int:
|
|
return self._token_limit
|
|
|
|
===== FILE: sia/llm_engine/openai_llm_engine.py =====
|
|
from typing import Callable, Iterator
|
|
import openai
|
|
import tiktoken
|
|
|
|
from . import LlmEngine
|
|
|
|
class OpenAILlmEngine(LlmEngine):
|
|
"""
|
|
LLM Engine implementation using OpenAI's API.
|
|
Supports streaming responses from chat completion models.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
model: str,
|
|
temperature: float,
|
|
token_limit: int,
|
|
api_key: str,
|
|
):
|
|
"""
|
|
Initialize the OpenAI LLM Engine.
|
|
|
|
Args:
|
|
model: OpenAI model to use
|
|
temperature: Temperature for sampling
|
|
api_key: OpenAI API key
|
|
token_limit: Maximum number of tokens to generate
|
|
"""
|
|
self._model = model
|
|
self._temperature = temperature
|
|
self._token_limit = token_limit
|
|
|
|
self._client = openai.Client(
|
|
api_key=api_key,
|
|
)
|
|
|
|
def infer(self, system_prompt: str, main_context: str, should_stop: Callable[[], bool] = lambda: False) -> Iterator[str]:
|
|
messages = [
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": main_context}
|
|
]
|
|
|
|
stream = self._client.chat.completions.create(
|
|
model=self._model,
|
|
messages=messages,
|
|
temperature=self._temperature,
|
|
stream=True,
|
|
)
|
|
|
|
try:
|
|
for chunk in stream:
|
|
if should_stop():
|
|
break
|
|
if content := chunk.choices[0].delta.content:
|
|
yield content
|
|
finally:
|
|
stream.close()
|
|
#stream.response.close()
|
|
|
|
def token_count(self, system_prompt: str, main_context: str) -> int:
|
|
"""
|
|
Calculate the total token count for the system prompt and context.
|
|
|
|
Args:
|
|
system_prompt: The system prompt string
|
|
main_context: The main context string
|
|
|
|
Returns:
|
|
int: Total number of tokens
|
|
"""
|
|
encoding = tiktoken.encoding_for_model(self._model)
|
|
return len(encoding.encode(system_prompt)) + len(encoding.encode(main_context))
|
|
|
|
def token_limit(self) -> int:
|
|
return self._token_limit
|
|
===== FILE: sia/response_parser.py =====
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
import xml.etree.ElementTree as ET
|
|
from typing import Union
|
|
|
|
from .util import format_timestamp
|
|
|
|
from .command import Command
|
|
from .delete_command import DeleteCommand
|
|
from .stop_command import StopCommand
|
|
|
|
from .entry.background_entry import BackgroundEntry
|
|
from .entry import Entry
|
|
from .io_buffer import IOBuffer
|
|
from .entry.parse_error_entry import ParseErrorEntry
|
|
from .entry.read_entry import ReadEntry
|
|
from .entry.reasoning_entry import ReasoningEntry
|
|
from .entry.repeat_entry import RepeatEntry
|
|
from .entry.single_entry import SingleEntry
|
|
from .entry.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, work_dir: Path, io_buffer: IOBuffer):
|
|
"""
|
|
Initialize parser with IO buffer.
|
|
|
|
Args:
|
|
work_dir: Workdir for the scripts
|
|
io_buffer: Buffer to use for IO operations
|
|
"""
|
|
self._work_dir = work_dir
|
|
self._io_buffer = io_buffer
|
|
|
|
@property
|
|
def io_buffer(self) -> IOBuffer:
|
|
"""
|
|
Get the IO buffer used by the parser.
|
|
|
|
Returns:
|
|
IOBuffer: Buffer used for IO operations
|
|
"""
|
|
return self._io_buffer
|
|
|
|
def parse(self, timestamp: datetime, 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 = format_timestamp(timestamp)
|
|
|
|
parser = ET.XMLPullParser(events=("start", "end"))
|
|
parser.feed(xml)
|
|
root = None
|
|
try:
|
|
for event, root in parser.read_events():
|
|
if event == "start":
|
|
break
|
|
except ET.ParseError as e:
|
|
return ParseErrorEntry(entry_id, xml, f"Invalid XML: {str(e)}")
|
|
|
|
try:
|
|
if root.tag == 'delete':
|
|
target_id = root.get('id')
|
|
if not target_id:
|
|
return ParseErrorEntry(entry_id, xml, "Delete command missing required 'id' attribute")
|
|
if len(root.attrib) > 1:
|
|
return ParseErrorEntry(entry_id, xml, "Delete command should only have 'id' attribute")
|
|
return DeleteCommand(target_id)
|
|
|
|
elif root.tag == 'stop':
|
|
return StopCommand()
|
|
|
|
elif root.tag == 'background':
|
|
if len(root) != 0 or root.attrib or root.text is None or root.text.strip() == '':
|
|
return ParseErrorEntry(entry_id, xml, "Background entry requires (only) script content")
|
|
return BackgroundEntry(entry_id, self._work_dir, root.text)
|
|
|
|
elif root.tag == 'repeat':
|
|
if len(root) != 0 or root.text is None or root.text.strip() == '':
|
|
return ParseErrorEntry(entry_id, xml, "Repeat entry requires (only) script content")
|
|
if len(root.attrib) > 2 or any(k not in ('timeout', 'limit') for k in root.attrib):
|
|
return ParseErrorEntry(entry_id, xml, "Repeat entry only accepts 'timeout' and 'limit' attributes")
|
|
timeout = root.get('timeout')
|
|
limit = root.get('limit')
|
|
timeout = float(timeout) if timeout is not None else None
|
|
limit = int(limit) if limit is not None else None
|
|
return RepeatEntry(entry_id, self._work_dir, root.text, timeout, limit)
|
|
|
|
elif root.tag == 'single':
|
|
if len(root) != 0 or root.text is None or root.text.strip() == '':
|
|
return ParseErrorEntry(entry_id, xml, "Single entry requires (only) script content")
|
|
if len(root.attrib) > 2 or any(k not in ('timeout', 'limit') for k in root.attrib):
|
|
return ParseErrorEntry(entry_id, xml, "Single entry only accepts 'timeout' and 'limit' attributes")
|
|
timeout = root.get('timeout')
|
|
limit = root.get('limit')
|
|
timeout = float(timeout) if timeout is not None else None
|
|
limit = int(limit) if limit is not None else None
|
|
return SingleEntry(entry_id, self._work_dir, root.text, timeout, limit)
|
|
|
|
elif root.tag == 'reasoning':
|
|
if len(root) != 0 or root.attrib or root.text is None or root.text.strip() == '':
|
|
return ParseErrorEntry(entry_id, xml, "Reasoning entry requires (only) text content")
|
|
return ReasoningEntry(entry_id, root.text)
|
|
|
|
elif root.tag == 'read_stdin':
|
|
return ReadEntry(entry_id, self._io_buffer)
|
|
|
|
elif root.tag == 'write_stdout':
|
|
if len(root) != 0 or root.attrib or root.text is None or root.text.strip() == '':
|
|
return ParseErrorEntry(entry_id, xml, "Write stdout entry requires (only) text content")
|
|
return WriteEntry(entry_id, root.text, self._io_buffer)
|
|
|
|
else:
|
|
return ParseErrorEntry(entry_id, xml, f"Unknown root element: {root.tag}")
|
|
|
|
except Exception as e:
|
|
return ParseErrorEntry(entry_id, xml, f"Error parsing response: {str(e)}")
|
|
|
|
===== FILE: sia/standard_io_buffer.py =====
|
|
import sys
|
|
|
|
from .io_buffer import IOBuffer
|
|
|
|
class StandardIOBuffer(IOBuffer):
|
|
"""
|
|
IOBuffer implementation that uses system standard input/output.
|
|
|
|
This class provides direct access to stdin/stdout for IO operations.
|
|
It implements a basic line-based input buffer to handle cases where
|
|
multiple reads are needed to process all available input.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize the standard IO buffer."""
|
|
self._input_buffer: str = ""
|
|
|
|
def read(self) -> str:
|
|
"""
|
|
Read available input from stdin.
|
|
|
|
If there is buffered input from a previous read, return that first.
|
|
Otherwise, try to read new input from stdin if available.
|
|
|
|
Returns:
|
|
str: Content read from stdin, or empty string if no input available
|
|
"""
|
|
# Return and clear any existing buffered input
|
|
if self._input_buffer:
|
|
content = self._input_buffer
|
|
self._input_buffer = ""
|
|
return content
|
|
|
|
# Check if there's input available
|
|
if not sys.stdin.isatty() and sys.stdin.readable():
|
|
try:
|
|
content = sys.stdin.read()
|
|
if content:
|
|
return content
|
|
except Exception:
|
|
pass # Ignore any read errors
|
|
|
|
return ""
|
|
|
|
def write(self, content: str) -> None:
|
|
"""
|
|
Write content to stdout.
|
|
|
|
Args:
|
|
content: String content to write
|
|
"""
|
|
if not content:
|
|
return
|
|
|
|
try:
|
|
sys.stdout.write(content)
|
|
sys.stdout.flush()
|
|
except Exception:
|
|
pass # Ignore write errors
|
|
|
|
def buffer_length(self) -> int:
|
|
"""
|
|
Get the current length of buffered input.
|
|
|
|
Returns:
|
|
int: Number of characters in the input buffer
|
|
"""
|
|
return len(self._input_buffer)
|
|
===== FILE: sia/stop_command.py =====
|
|
from .command import Command
|
|
from .command_result import CommandResult
|
|
from .working_memory import WorkingMemory
|
|
|
|
class StopCommand(Command):
|
|
"""
|
|
Command to stop the agent.
|
|
Performs cleanup on all entries and clears working memory.
|
|
"""
|
|
|
|
def execute(self, memory: WorkingMemory) -> CommandResult:
|
|
"""
|
|
Signal that the agent should stop.
|
|
Cleans up all entries and clears the working memory.
|
|
|
|
Args:
|
|
memory: WorkingMemory instance to clear
|
|
|
|
Returns:
|
|
CommandResult: Stop result
|
|
"""
|
|
# Get a copy of entries to iterate over
|
|
entries = memory.get_entries()
|
|
|
|
# Clean up each entry individually
|
|
for entry in entries:
|
|
entry.cleanup()
|
|
memory.remove_entry(entry.id)
|
|
|
|
return CommandResult.stop()
|
|
===== FILE: sia/system_metrics.py =====
|
|
import time
|
|
from typing import Dict
|
|
import psutil
|
|
from .util import format_timestamp
|
|
|
|
class SystemMetrics:
|
|
"""
|
|
Tracks system metrics including memory and disk usage.
|
|
|
|
"""
|
|
|
|
def get_metrics(self) -> Dict:
|
|
"""
|
|
Get current system metrics.
|
|
Clears usage samples after calculating averages.
|
|
|
|
Returns:
|
|
Dict containing:
|
|
- timestamp: Current timestamp
|
|
- memory_used: Used memory in bytes
|
|
- memory_total: Total memory in bytes
|
|
- disk_used: Used disk space in bytes
|
|
- disk_total: Total disk space in bytes
|
|
"""
|
|
metrics = {}
|
|
|
|
# Add timestamp
|
|
metrics["timestamp"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
|
|
# Memory usage in bytes
|
|
memory = psutil.virtual_memory()
|
|
metrics["memory_used"] = memory.used
|
|
metrics["memory_total"] = memory.total
|
|
|
|
# Disk usage in bytes (root partition)
|
|
disk = psutil.disk_usage('/')
|
|
metrics["disk_used"] = disk.used
|
|
metrics["disk_total"] = disk.total
|
|
|
|
return metrics
|
|
===== FILE: sia/util.py =====
|
|
import datetime
|
|
import xml.dom.minidom
|
|
import xml.etree.ElementTree as ET
|
|
from typing import Iterator, Optional
|
|
|
|
def stop_before_value(iterator: Iterator[str], stop_value: str) -> Iterator[str]:
|
|
"""
|
|
Creates an iterator that yields values from the input iterator
|
|
until it encounters the stop_value (exclusive).
|
|
|
|
Args:
|
|
iterator: The source iterator
|
|
stop_value: The value to stop before
|
|
|
|
Yields:
|
|
Values from the iterator until stop_value is encountered
|
|
If stop_value is part of an item, yields the part before stop_value
|
|
"""
|
|
for item in iterator:
|
|
if stop_value in item:
|
|
split_point = item.index(stop_value)
|
|
if split_point > 0:
|
|
yield item[:split_point]
|
|
break
|
|
yield item
|
|
|
|
def pretty_print_element(elem: ET.Element, level: int = 0, max_line: int = 80) -> str:
|
|
"""Convert ElementTree element to pretty-printed string with custom formatting."""
|
|
indent = ' ' * level
|
|
|
|
# Handle empty elements
|
|
if not elem.text and not elem.tail and not elem.attrib and len(elem) == 0:
|
|
return f'{indent}<{elem.tag}/>'
|
|
|
|
# Build opening tag with attributes
|
|
tag = elem.tag
|
|
attrs = elem.attrib
|
|
if attrs:
|
|
attr_strings = [f'{k}="{v}"' for k, v in sorted(attrs.items())]
|
|
attr_str = ' ' + ' '.join(attr_strings)
|
|
|
|
if len(indent) + len(tag) + len(attr_str) + 2 > max_line:
|
|
attr_indent = indent + ' '
|
|
attr_str = '\n' + '\n'.join(f'{attr_indent}{a}' for a in attr_strings)
|
|
else:
|
|
attr_str = ''
|
|
|
|
parts = [f'{indent}<{tag}{attr_str}>']
|
|
|
|
# Handle text content
|
|
if elem.text and isinstance(elem.text, str) and elem.text.strip():
|
|
text = elem.text
|
|
if ']]>' in text:
|
|
escaped = text.replace('&', '&') \
|
|
.replace('<', '<') \
|
|
.replace('>', '>') \
|
|
.replace('"', '"') \
|
|
.replace("'", ''')
|
|
parts.append(f'{indent} {escaped}')
|
|
else:
|
|
parts.append(f'{indent} <![CDATA[{text}]]>')
|
|
|
|
# Handle children
|
|
for child in elem:
|
|
parts.append(pretty_print_element(child, level + 1, max_line))
|
|
if child.tail and child.tail.strip():
|
|
parts.append(f'{indent} {child.tail}')
|
|
|
|
parts.append(f'{indent}</{tag}>')
|
|
return '\n'.join(parts)
|
|
|
|
def format_timestamp(timestamp: datetime) -> str:
|
|
return timestamp.strftime("%Y%m%d_%H%M%S_%f")[:-3]
|
|
===== FILE: sia/web/api.py =====
|
|
from pathlib import Path
|
|
from aiohttp import web
|
|
import json
|
|
import asyncio
|
|
|
|
|
|
from ..auto_approver import AutoApprover
|
|
from ..entry.entry_factory import EntryFactory
|
|
from ..iteration_parser import IterationParser
|
|
from ..web_agent import WebAgent
|
|
from ..web_io_buffer import WebIOBuffer
|
|
from ..working_memory import WorkingMemory
|
|
|
|
class Api:
|
|
def __init__(
|
|
self,
|
|
work_dir: Path,
|
|
app: web.Application,
|
|
agent: WebAgent,
|
|
io_buffer: WebIOBuffer,
|
|
working_memory: WorkingMemory,
|
|
auto_approver: AutoApprover
|
|
):
|
|
self._work_dir = work_dir
|
|
self._app = app
|
|
self._agent = agent
|
|
self._working_memory = working_memory
|
|
self._io_buffer = io_buffer
|
|
self._auto_approver = auto_approver
|
|
|
|
self._init_routes()
|
|
|
|
def _init_routes(self):
|
|
"""Initialize REST API and WebSocket routes."""
|
|
self._app.router.add_post("/api/inference/{llm}", self._run_inference)
|
|
self._app.router.add_post("/api/inference/{llm}/stop", self._stop_inference)
|
|
self._app.router.add_post("/api/approve/{llm}", self._approve_response)
|
|
self._app.router.add_post("/api/context", self._modify_context)
|
|
self._app.router.add_post("/api/input", self._send_input)
|
|
self._app.router.add_post("/api/clear", self._clear_output)
|
|
self._app.router.add_get("/api/output/{llm}", self._get_output)
|
|
self._app.router.add_get("/api/llms", self._get_llms)
|
|
self._app.router.add_get("/api/auto_approver/config", self._get_auto_approver_config)
|
|
self._app.router.add_post("/api/auto_approver/config", self._set_auto_approver_config)
|
|
self._app.router.add_post("/api/auto_approver/context_enabled", self._set_context_enabled)
|
|
self._app.router.add_post("/api/auto_approver/response_enabled", self._set_response_enabled)
|
|
self._app.router.add_post("/api/auto_approver/context_timeout", self._set_context_timeout)
|
|
self._app.router.add_post("/api/auto_approver/response_timeout", self._set_response_timeout)
|
|
self._app.router.add_post("/api/auto_approver/llm", self._set_llm_name)
|
|
self._app.router.add_get("/api/memory", self._get_memory)
|
|
self._app.router.add_post("/api/memory/entry", self._create_entry)
|
|
self._app.router.add_put("/api/memory/entry/{id}", self._save_entry)
|
|
self._app.router.add_delete("/api/memory/entry/{id}", self._delete_entry)
|
|
self._app.router.add_post("/api/memory/entry/{id}/reset", self._reset_entry)
|
|
self._app.router.add_post("/api/memory/entry/{id}/update", self._update_entry)
|
|
self._app.router.add_post("/api/memory/load_iteration", self._load_iteration)
|
|
|
|
async def _run_inference(self, request: web.Request) -> web.Response:
|
|
"""Start inference on specified LLM."""
|
|
llm_name = request.match_info["llm"]
|
|
try:
|
|
await asyncio.get_event_loop().run_in_executor(None, self._agent.run_inference, llm_name)
|
|
return web.Response(status=200)
|
|
except (ValueError, RuntimeError) as e:
|
|
return web.Response(status=400, text=str(e))
|
|
|
|
async def _stop_inference(self, request: web.Request) -> web.Response:
|
|
"""Stop inference on specified LLM."""
|
|
llm_name = request.match_info["llm"]
|
|
try:
|
|
self._agent.stop_inference(llm_name)
|
|
return web.Response(status=200)
|
|
except ValueError as e:
|
|
return web.Response(status=400, text=str(e))
|
|
|
|
async def _approve_response(self, request: web.Request) -> web.Response:
|
|
"""Approve response from specified LLM."""
|
|
llm_name = request.match_info["llm"]
|
|
data = await request.json()
|
|
response = data.get("response")
|
|
if not response:
|
|
return web.Response(status=400, text="Missing response in request body")
|
|
try:
|
|
self._agent.approve_response(llm_name, response)
|
|
return web.Response(status=200)
|
|
except ValueError as e:
|
|
return web.Response(status=400, text=str(e))
|
|
|
|
async def _modify_context(self, request: web.Request) -> web.Response:
|
|
"""Modify the current context."""
|
|
data = await request.json()
|
|
context = data.get("context")
|
|
if not context:
|
|
return web.Response(status=400, text="Missing context in request body")
|
|
self._agent.modify_context(context)
|
|
return web.Response(status=200)
|
|
|
|
async def _send_input(self, request: web.Request) -> web.Response:
|
|
"""Send input to the IO buffer."""
|
|
data = await request.json()
|
|
input_text = data.get("input")
|
|
if not input_text:
|
|
return web.Response(status=400, text="Missing input in request body")
|
|
self._io_buffer.append_stdin(input_text)
|
|
return web.Response(status=200)
|
|
|
|
async def _clear_output(self, request: web.Request) -> web.Response:
|
|
"""Clear the stdout buffer."""
|
|
self._io_buffer.clear_stdout()
|
|
return web.Response(status=200)
|
|
|
|
async def _get_output(self, request: web.Request) -> web.Response:
|
|
"""Get complete output for specified LLM."""
|
|
llm_name = request.match_info["llm"]
|
|
try:
|
|
output = self._agent.get_output(llm_name)
|
|
return web.Response(
|
|
text=json.dumps({"output": output}),
|
|
content_type="application/json"
|
|
)
|
|
except ValueError as e:
|
|
return web.Response(status=400, text=str(e))
|
|
|
|
async def _get_llms(self, request: web.Request) -> web.Response:
|
|
"""Get all LLMs and their current states."""
|
|
states = self._agent.llms
|
|
return web.Response(
|
|
text=json.dumps({
|
|
name: state.name
|
|
for name, state in states.items()
|
|
}),
|
|
content_type="application/json"
|
|
)
|
|
|
|
async def _get_auto_approver_config(self, request: web.Request) -> web.Response:
|
|
"""Get current auto approver configuration."""
|
|
return web.Response(
|
|
text=json.dumps(self._auto_approver.config),
|
|
content_type="application/json"
|
|
)
|
|
|
|
async def _set_auto_approver_config(self, request: web.Request) -> web.Response:
|
|
"""Update auto approver configuration."""
|
|
data = await request.json()
|
|
try:
|
|
self._auto_approver.set_config(data)
|
|
return web.Response(status=200)
|
|
except (ValueError, KeyError) as e:
|
|
return web.Response(status=400, text=str(e))
|
|
|
|
async def _set_context_enabled(self, request: web.Request) -> web.Response:
|
|
"""Set context auto-approval enabled state."""
|
|
data = await request.json()
|
|
enabled = data.get("enabled")
|
|
if enabled is None:
|
|
return web.Response(status=400, text="Missing enabled parameter")
|
|
try:
|
|
self._auto_approver.context_enabled = enabled
|
|
return web.Response(status=200)
|
|
except ValueError as e:
|
|
return web.Response(status=400, text=str(e))
|
|
|
|
async def _set_response_enabled(self, request: web.Request) -> web.Response:
|
|
"""Set response auto-approval enabled state."""
|
|
data = await request.json()
|
|
enabled = data.get("enabled")
|
|
if enabled is None:
|
|
return web.Response(status=400, text="Missing enabled parameter")
|
|
try:
|
|
self._auto_approver.response_enabled = enabled
|
|
return web.Response(status=200)
|
|
except ValueError as e:
|
|
return web.Response(status=400, text=str(e))
|
|
|
|
async def _set_context_timeout(self, request: web.Request) -> web.Response:
|
|
"""Set context auto-approval timeout."""
|
|
data = await request.json()
|
|
timeout = data.get("timeout")
|
|
if timeout is None:
|
|
return web.Response(status=400, text="Missing timeout parameter")
|
|
try:
|
|
self._auto_approver.context_timeout = float(timeout)
|
|
return web.Response(status=200)
|
|
except ValueError as e:
|
|
return web.Response(status=400, text=str(e))
|
|
|
|
async def _set_response_timeout(self, request: web.Request) -> web.Response:
|
|
"""Set response auto-approval timeout."""
|
|
data = await request.json()
|
|
timeout = data.get("timeout")
|
|
if timeout is None:
|
|
return web.Response(status=400, text="Missing timeout parameter")
|
|
try:
|
|
self._auto_approver.response_timeout = float(timeout)
|
|
return web.Response(status=200)
|
|
except ValueError as e:
|
|
return web.Response(status=400, text=str(e))
|
|
|
|
async def _set_llm_name(self, request: web.Request) -> web.Response:
|
|
"""Set LLM name for auto-approval."""
|
|
data = await request.json()
|
|
name = data.get("name")
|
|
if name is None:
|
|
return web.Response(status=400, text="Missing name parameter")
|
|
try:
|
|
self._auto_approver.llm_name = name
|
|
return web.Response(status=200)
|
|
except ValueError as e:
|
|
return web.Response(status=400, text=str(e))
|
|
|
|
async def _get_memory(self, request: web.Request) -> web.Response:
|
|
"""Get complete working memory state."""
|
|
entries = self._working_memory.get_entries()
|
|
return web.Response(
|
|
text=json.dumps([e.serialize() for e in entries]),
|
|
content_type="application/json"
|
|
)
|
|
|
|
async def _create_entry(self, request: web.Request) -> web.Response:
|
|
"""Create a new entry in working memory."""
|
|
data = await request.json()
|
|
try:
|
|
entry = EntryFactory.create_entry(data, self._work_dir, self._io_buffer)
|
|
self._working_memory.add_entry(entry)
|
|
return web.Response(
|
|
text=json.dumps({"id": entry.id}),
|
|
content_type="application/json"
|
|
)
|
|
except (ValueError, TypeError) as e:
|
|
return web.Response(status=400, text=str(e))
|
|
|
|
async def _save_entry(self, request: web.Request) -> web.Response:
|
|
"""Update properties of an existing entry."""
|
|
entry_id = request.match_info["id"]
|
|
data = await request.json()
|
|
entry = self._working_memory.get_entry(entry_id)
|
|
if not entry:
|
|
return web.Response(status=404, text="Entry not found")
|
|
try:
|
|
EntryFactory.update_entry(entry, data)
|
|
entry.notify_change()
|
|
return web.Response(status=200)
|
|
except ValueError as e:
|
|
return web.Response(status=400, text=str(e))
|
|
|
|
async def _delete_entry(self, request: web.Request) -> web.Response:
|
|
"""Delete an entry from working memory."""
|
|
entry_id = request.match_info["id"]
|
|
self._working_memory.remove_entry(entry_id)
|
|
return web.Response(status=200)
|
|
|
|
async def _reset_entry(self, request: web.Request) -> web.Response:
|
|
"""Reset an entry's state."""
|
|
entry_id = request.match_info["id"]
|
|
entry = self._working_memory.get_entry(entry_id)
|
|
if not entry:
|
|
return web.Response(status=404, text="Entry not found")
|
|
entry.reset()
|
|
return web.Response(status=200)
|
|
|
|
async def _update_entry(self, request: web.Request) -> web.Response:
|
|
"""Update an entry's state."""
|
|
entry_id = request.match_info["id"]
|
|
entry = self._working_memory.get_entry(entry_id)
|
|
if not entry:
|
|
return web.Response(status=404, text="Entry not found")
|
|
try:
|
|
entry.update()
|
|
entry.notify_change()
|
|
return web.Response(status=200)
|
|
except ValueError as e:
|
|
return web.Response(status=400, text=str(e))
|
|
|
|
async def _load_iteration(self, request: web.Request) -> web.Response:
|
|
"""Load entries from iteration XML content into working memory"""
|
|
data = await request.json()
|
|
content = data.get("content")
|
|
if not content:
|
|
return web.Response(status=400, text="Missing content in request body")
|
|
|
|
entries = IterationParser.parse_iteration(content, self._work_dir, self._io_buffer)
|
|
|
|
for entry in entries:
|
|
self._working_memory.add_entry(entry)
|
|
|
|
return web.Response(status=200)
|
|
|
|
===== FILE: sia/web/auto_approver_websocket.py =====
|
|
from aiohttp import web, WSMsgType
|
|
from typing import Dict, Set
|
|
|
|
from ..auto_approver import AutoApprover, AutoApproverConfig
|
|
from .util import wrap_async
|
|
|
|
class AutoApproverWebSocket:
|
|
"""
|
|
WebSocket handler for AutoApprover configuration.
|
|
Broadcasts config updates to all connected clients.
|
|
"""
|
|
|
|
def __init__(self, auto_approver: AutoApprover):
|
|
self._auto_approver = auto_approver
|
|
self._clients: Set[web.WebSocketResponse] = set()
|
|
self._auto_approver.add_config_change_handler(wrap_async(self._handle_config_change))
|
|
|
|
async def _broadcast_message(self, message: Dict):
|
|
"""Broadcast message to all connected clients."""
|
|
disconnected = set()
|
|
for ws in self._clients:
|
|
try:
|
|
await ws.send_json(message)
|
|
except ConnectionResetError:
|
|
disconnected.add(ws)
|
|
self._clients -= disconnected
|
|
|
|
async def _handle_config_change(self, config: AutoApproverConfig):
|
|
"""Handle config changes from the AutoApprover."""
|
|
await self._broadcast_message({
|
|
"config": config
|
|
})
|
|
|
|
async def handle_connection(self, request: web.Request) -> web.WebSocketResponse:
|
|
"""Handle new WebSocket connections."""
|
|
ws = web.WebSocketResponse(heartbeat=30)
|
|
await ws.prepare(request)
|
|
|
|
self._clients.add(ws)
|
|
|
|
try:
|
|
# Send initial config
|
|
await ws.send_json({
|
|
"config": self._auto_approver.config
|
|
})
|
|
|
|
async for msg in ws:
|
|
if msg.type == WSMsgType.ERROR:
|
|
print(f"WebSocket connection closed with error: {ws.exception()}")
|
|
finally:
|
|
self._clients.remove(ws)
|
|
|
|
return ws
|
|
|
|
===== FILE: sia/web/context_websocket.py =====
|
|
from aiohttp import web, WSMsgType
|
|
from typing import Dict, Set
|
|
|
|
from .util import wrap_async
|
|
from ..web_agent import WebAgent
|
|
|
|
class ContextWebSocket:
|
|
"""
|
|
WebSocket handler for context changes.
|
|
Broadcasts context updates to all connected clients.
|
|
"""
|
|
|
|
def __init__(self, agent: WebAgent):
|
|
self._agent = agent
|
|
self._clients: Set[web.WebSocketResponse] = set()
|
|
self._agent.add_context_change_handler(wrap_async(self._handle_context_change))
|
|
|
|
async def _broadcast_message(self, message: Dict):
|
|
"""Broadcast message to all connected clients."""
|
|
disconnected = set()
|
|
for ws in self._clients:
|
|
try:
|
|
await ws.send_json(message)
|
|
except ConnectionResetError:
|
|
disconnected.add(ws)
|
|
self._clients -= disconnected
|
|
|
|
async def _handle_context_change(self, context: str, generated: bool):
|
|
"""Handle context changes from the WebAgent."""
|
|
await self._broadcast_message({
|
|
"context": context,
|
|
"generated": generated
|
|
})
|
|
|
|
async def handle_connection(self, request: web.Request) -> web.WebSocketResponse:
|
|
"""Handle new WebSocket connections."""
|
|
ws = web.WebSocketResponse(heartbeat=30)
|
|
await ws.prepare(request)
|
|
|
|
self._clients.add(ws)
|
|
|
|
try:
|
|
# Send initial context
|
|
await ws.send_json({
|
|
"context": self._agent.context,
|
|
"generated": True
|
|
})
|
|
|
|
async for msg in ws:
|
|
if msg.type == WSMsgType.ERROR:
|
|
print(f"WebSocket connection closed with error: {ws.exception()}")
|
|
finally:
|
|
self._clients.remove(ws)
|
|
|
|
return ws
|
|
|
|
===== FILE: sia/web/llm_websocket.py =====
|
|
from aiohttp import web, WSMsgType
|
|
from typing import Dict, Set
|
|
|
|
from .util import wrap_async
|
|
from ..web_agent import WebAgent, LlmState
|
|
|
|
class LlmWebSocket:
|
|
"""
|
|
WebSocket handler for LLM state changes.
|
|
Broadcasts state updates to all connected clients.
|
|
"""
|
|
|
|
def __init__(self, agent: WebAgent):
|
|
self._agent = agent
|
|
self._clients: Set[web.WebSocketResponse] = set()
|
|
self._agent.add_llm_change_handler(wrap_async(self._handle_state_change))
|
|
|
|
async def _broadcast_message(self, message: Dict):
|
|
"""Broadcast message to all connected clients."""
|
|
disconnected = set()
|
|
for ws in self._clients:
|
|
try:
|
|
await ws.send_json(message)
|
|
except ConnectionResetError:
|
|
disconnected.add(ws)
|
|
self._clients -= disconnected
|
|
|
|
async def _handle_state_change(self, llm_name: str, new_state: LlmState):
|
|
"""Handle state changes from the WebAgent."""
|
|
await self._broadcast_message({
|
|
"llm": llm_name,
|
|
"state": new_state.name
|
|
})
|
|
|
|
async def handle_connection(self, request: web.Request) -> web.WebSocketResponse:
|
|
"""Handle new WebSocket connections."""
|
|
ws = web.WebSocketResponse(heartbeat=30)
|
|
await ws.prepare(request)
|
|
|
|
try:
|
|
# Send initial states for all LLMs
|
|
states = self._agent.llms
|
|
for llm_name, state in states.items():
|
|
await ws.send_json({
|
|
"llm": llm_name,
|
|
"state": state.name
|
|
})
|
|
|
|
self._clients.add(ws)
|
|
|
|
async for msg in ws:
|
|
if msg.type == WSMsgType.ERROR:
|
|
print(f"WebSocket connection closed with error: {ws.exception()}")
|
|
finally:
|
|
self._clients.remove(ws)
|
|
|
|
return ws
|
|
===== FILE: sia/web/memory_websocket.py =====
|
|
from aiohttp import web, WSMsgType
|
|
from typing import Dict, Set
|
|
|
|
from ..entry import Entry
|
|
from ..web_agent import WebAgent
|
|
from ..working_memory import WorkingMemory
|
|
from .util import wrap_async
|
|
|
|
class MemoryWebSocket:
|
|
"""
|
|
WebSocket handler for working memory changes.
|
|
Broadcasts memory updates to all connected clients.
|
|
"""
|
|
|
|
def __init__(self, working_memory: WorkingMemory):
|
|
self._working_memory = working_memory
|
|
self._clients: Set[web.WebSocketResponse] = set()
|
|
self._working_memory.add_change_handler(wrap_async(self._handle_memory_change))
|
|
|
|
async def _broadcast_message(self, message: Dict):
|
|
"""Broadcast message to all connected clients."""
|
|
disconnected = set()
|
|
for ws in self._clients:
|
|
try:
|
|
await ws.send_json(message)
|
|
except ConnectionResetError:
|
|
disconnected.add(ws)
|
|
self._clients -= disconnected
|
|
|
|
async def _handle_memory_change(self):
|
|
"""Handle working memory changes."""
|
|
entries = self._working_memory.get_entries()
|
|
await self._broadcast_message({
|
|
"type": "memory_state",
|
|
"entries": [e.serialize() for e in entries]
|
|
})
|
|
|
|
async def handle_connection(self, request: web.Request) -> web.WebSocketResponse:
|
|
"""Handle new WebSocket connections."""
|
|
ws = web.WebSocketResponse(heartbeat=30)
|
|
await ws.prepare(request)
|
|
|
|
try:
|
|
# Send initial memory state
|
|
entries = self._working_memory.get_entries()
|
|
await ws.send_json({
|
|
"type": "memory_state",
|
|
"entries": [e.serialize() for e in entries]
|
|
})
|
|
|
|
self._clients.add(ws)
|
|
|
|
async for msg in ws:
|
|
if msg.type == WSMsgType.ERROR:
|
|
print(f"WebSocket connection closed with error: {ws.exception()}")
|
|
finally:
|
|
self._clients.remove(ws)
|
|
|
|
return ws
|
|
|
|
===== FILE: sia/web/static.py =====
|
|
from aiohttp import web
|
|
import mimetypes
|
|
|
|
from ..config import Config
|
|
|
|
mimetypes.add_type("application/javascript", ".js")
|
|
mimetypes.add_type("application/javascript", ".jsx")
|
|
mimetypes.add_type("text/javascript", ".js")
|
|
mimetypes.add_type("text/javascript", ".jsx")
|
|
|
|
class Static:
|
|
def __init__(self, app: web.Application, config: Config):
|
|
self._config = config
|
|
|
|
app.router.add_get("/", self._serve_index)
|
|
app.router.add_static("/static/", self._config.static_files, show_index=False)
|
|
app.router.add_static("/assets/", self._config.static_files / "assets", show_index=False)
|
|
app.router.add_get("/{path:.*}", self._serve_index)
|
|
|
|
async def _serve_index(self, request: web.Request) -> web.Response:
|
|
"""Serve the React application HTML for any unmatched routes."""
|
|
index_path = self._config.static_files / "index.html"
|
|
if not index_path.exists():
|
|
raise web.HTTPNotFound()
|
|
|
|
with open(index_path, "r") as f:
|
|
html_content = f.read()
|
|
|
|
return web.Response(
|
|
text=html_content,
|
|
content_type="text/html"
|
|
)
|
|
===== FILE: sia/web/stdout_websocket.py =====
|
|
from aiohttp import web, WSMsgType
|
|
from typing import Dict, Set
|
|
import asyncio
|
|
|
|
from .util import wrap_async
|
|
from ..web_io_buffer import WebIOBuffer
|
|
|
|
class StdoutWebSocket:
|
|
"""
|
|
WebSocket handler for stdout changes.
|
|
Broadcasts stdout updates to all connected clients.
|
|
"""
|
|
|
|
def __init__(self, io_buffer: WebIOBuffer):
|
|
self._io_buffer = io_buffer
|
|
self._clients: Set[web.WebSocketResponse] = set()
|
|
self._io_buffer.add_stdout_change_handler(wrap_async(self._handle_stdout_change))
|
|
|
|
async def _broadcast_message(self, message: Dict):
|
|
"""Broadcast message to all connected clients."""
|
|
disconnected = set()
|
|
for ws in self._clients:
|
|
try:
|
|
await ws.send_json(message)
|
|
except ConnectionResetError:
|
|
disconnected.add(ws)
|
|
self._clients -= disconnected
|
|
|
|
async def _handle_stdout_change(self, output: str):
|
|
"""Handle stdout changes from the WebIOBuffer."""
|
|
await self._broadcast_message({
|
|
"output": output
|
|
})
|
|
|
|
async def handle_connection(self, request: web.Request) -> web.WebSocketResponse:
|
|
"""Handle new WebSocket connections."""
|
|
ws = web.WebSocketResponse(heartbeat=30)
|
|
await ws.prepare(request)
|
|
|
|
self._clients.add(ws)
|
|
|
|
try:
|
|
# Send initial stdout content
|
|
await ws.send_json({
|
|
"output": self._io_buffer.get_stdout()
|
|
})
|
|
|
|
async for msg in ws:
|
|
if msg.type == WSMsgType.ERROR:
|
|
print(f"WebSocket connection closed with error: {ws.exception()}")
|
|
finally:
|
|
self._clients.remove(ws)
|
|
|
|
return ws
|
|
|
|
===== FILE: sia/web/token_websocket.py =====
|
|
from aiohttp import web, WSMsgType
|
|
from typing import Dict, Set
|
|
import asyncio
|
|
import json
|
|
|
|
from .util import wrap_async
|
|
from ..web_agent import WebAgent
|
|
|
|
class TokenWebSocket:
|
|
"""
|
|
WebSocket handler for LLM token streaming.
|
|
Broadcasts new tokens to all connected clients.
|
|
"""
|
|
|
|
def __init__(self, agent: WebAgent):
|
|
self._agent = agent
|
|
self._clients: Set[web.WebSocketResponse] = set()
|
|
self._agent.add_token_handler(wrap_async(self._handle_new_token))
|
|
|
|
async def _broadcast_message(self, message: Dict):
|
|
"""Broadcast message to all connected clients."""
|
|
disconnected = set()
|
|
for ws in self._clients:
|
|
try:
|
|
await ws.send_json(message)
|
|
except ConnectionResetError:
|
|
disconnected.add(ws)
|
|
self._clients -= disconnected
|
|
|
|
async def _handle_new_token(self, llm_name: str, token: str):
|
|
"""Handle new tokens from the WebAgent."""
|
|
await self._broadcast_message({
|
|
"llm": llm_name,
|
|
"token": token
|
|
})
|
|
|
|
async def handle_connection(self, request: web.Request) -> web.WebSocketResponse:
|
|
"""Handle new WebSocket connections."""
|
|
ws = web.WebSocketResponse(heartbeat=30)
|
|
await ws.prepare(request)
|
|
|
|
self._clients.add(ws)
|
|
|
|
try:
|
|
async for msg in ws:
|
|
if msg.type == WSMsgType.ERROR:
|
|
print(f"WebSocket connection closed with error: {ws.exception()}")
|
|
finally:
|
|
self._clients.remove(ws)
|
|
|
|
return ws
|
|
|
|
===== FILE: sia/web/util.py =====
|
|
import asyncio
|
|
|
|
def wrap_async(coro_func):
|
|
"""Wraps an async callback to be safely called from another thread."""
|
|
loop = asyncio.get_event_loop()
|
|
def wrapper(*args, **kwargs):
|
|
loop.call_soon_threadsafe(lambda: asyncio.create_task(coro_func(*args, **kwargs)))
|
|
return wrapper
|
|
===== FILE: sia/web/websockts.py =====
|
|
from aiohttp import web
|
|
|
|
from ..auto_approver import AutoApprover
|
|
from ..web_agent import WebAgent
|
|
from ..web_io_buffer import WebIOBuffer
|
|
from ..working_memory import WorkingMemory
|
|
from .auto_approver_websocket import AutoApproverWebSocket
|
|
from .context_websocket import ContextWebSocket
|
|
from .llm_websocket import LlmWebSocket
|
|
from .memory_websocket import MemoryWebSocket
|
|
from .stdout_websocket import StdoutWebSocket
|
|
from .token_websocket import TokenWebSocket
|
|
|
|
class Websockets:
|
|
def __init__(self, app: web.Application, agent: WebAgent, io_buffer: WebIOBuffer, auto_approver: AutoApprover, working_memory: WorkingMemory):
|
|
self._llm_ws = LlmWebSocket(agent)
|
|
self._context_ws = ContextWebSocket(agent)
|
|
self._token_ws = TokenWebSocket(agent)
|
|
self._stdout_ws = StdoutWebSocket(io_buffer)
|
|
self._auto_approver_ws = AutoApproverWebSocket(auto_approver)
|
|
self._memory_ws = MemoryWebSocket(working_memory)
|
|
|
|
app.router.add_get("/ws/llm", self._llm_ws.handle_connection)
|
|
app.router.add_get("/ws/context", self._context_ws.handle_connection)
|
|
app.router.add_get("/ws/token", self._token_ws.handle_connection)
|
|
app.router.add_get("/ws/stdout", self._stdout_ws.handle_connection)
|
|
app.router.add_get("/ws/auto_approver", self._auto_approver_ws.handle_connection)
|
|
app.router.add_get("/ws/memory", self._memory_ws.handle_connection)
|
|
|
|
===== FILE: sia/web_agent.py =====
|
|
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_parser import ResponseParser
|
|
from .system_metrics import SystemMetrics
|
|
from .working_memory import WorkingMemory
|
|
from .xml_validator import XMLValidator
|
|
|
|
class LlmState(Enum):
|
|
NO_OUTPUT = auto()
|
|
INFERENCE = auto()
|
|
OUTPUT = 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.NO_OUTPUT for name in llms}
|
|
self._llm_outputs: Dict[str, str] = defaultdict(str)
|
|
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]] = []
|
|
|
|
# Working memory change handler
|
|
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 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_token_handler(self, handler: Callable[[str, str], None]) -> None:
|
|
"""Add handler for new tokens"""
|
|
if handler not in self._token_handlers:
|
|
self._token_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._llm_outputs.clear()
|
|
for llm_name in self._llms:
|
|
self._set_llm_state(llm_name, LlmState.NO_OUTPUT)
|
|
|
|
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.NO_OUTPUT:
|
|
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, should_stop)
|
|
|
|
with self._output_lock:
|
|
self._llm_outputs[llm_name] = ""
|
|
|
|
for token in response_token_iter:
|
|
with self._output_lock:
|
|
self._llm_outputs[llm_name] += token
|
|
|
|
for handler in self._token_handlers:
|
|
handler(llm_name, token)
|
|
|
|
with self._llm_lock:
|
|
self._set_llm_state(llm_name, LlmState.OUTPUT)
|
|
|
|
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 get_output(self, llm_name: str) -> str:
|
|
"""Get complete output for specified LLM"""
|
|
if llm_name not in self._llms:
|
|
raise ValueError(f"Unknown LLM: {llm_name}")
|
|
|
|
with self._output_lock:
|
|
return self._llm_outputs[llm_name]
|
|
|
|
def approve_response(self, llm_name: str, response: str) -> None:
|
|
"""Process approved response from specified LLM"""
|
|
if llm_name not in self._llms:
|
|
raise ValueError(f"Unknown LLM: {llm_name}")
|
|
|
|
timestamp = datetime.now(timezone.utc)
|
|
self._iteration_logger.log_iteration(timestamp, self._context, response)
|
|
parse_result = self._parser.parse(timestamp, response)
|
|
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)
|
|
|
|
===== FILE: sia/web_io_buffer.py =====
|
|
from threading import Lock
|
|
from typing import Callable, List
|
|
|
|
from .io_buffer import IOBuffer
|
|
|
|
class WebIOBuffer(IOBuffer):
|
|
"""
|
|
Thread-safe WebIOBuffer that maintains the synchronous IOBuffer interface.
|
|
Uses threading primitives instead of asyncio for synchronization.
|
|
"""
|
|
def __init__(self):
|
|
self._stdin_buffer = ""
|
|
self._stdout_buffer = ""
|
|
self._stdout_change_handlers: List[Callable[[str], None]] = []
|
|
|
|
def add_stdout_change_handler(self, handler: Callable[[str], None]) -> None:
|
|
"""
|
|
Add a callback for stdout changes.
|
|
"""
|
|
if handler not in self._stdout_change_handlers:
|
|
self._stdout_change_handlers.append(handler)
|
|
|
|
def read(self) -> str:
|
|
"""Thread-safe read from stdin buffer."""
|
|
content = self._stdin_buffer
|
|
self._stdin_buffer = ""
|
|
return content
|
|
|
|
def write(self, content: str) -> None:
|
|
"""Thread-safe write to stdout buffer."""
|
|
self._stdout_buffer += content
|
|
for handler in self._stdout_change_handlers:
|
|
handler(self._stdout_buffer)
|
|
|
|
def buffer_length(self) -> int:
|
|
"""
|
|
Get the current length of the stdin buffer.
|
|
|
|
Returns:
|
|
int: Number of characters in the stdin buffer
|
|
"""
|
|
return len(self._stdin_buffer)
|
|
|
|
def append_stdin(self, content: str) -> None:
|
|
"""
|
|
Append content to the stdin buffer.
|
|
|
|
Args:
|
|
content: String content to append to the stdin buffer
|
|
"""
|
|
self._stdin_buffer += content
|
|
|
|
def get_stdout(self) -> str:
|
|
"""Thread-safe get stdout content."""
|
|
return self._stdout_buffer
|
|
|
|
def clear_stdout(self) -> None:
|
|
"""Thread-safe clear stdout buffer."""
|
|
self._stdout_buffer = ""
|
|
for handler in self._stdout_change_handlers:
|
|
handler(self._stdout_buffer)
|
|
===== FILE: sia/working_memory.py =====
|
|
from typing import List, Optional, Callable, Set
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from .entry import Entry
|
|
|
|
class WorkingMemory:
|
|
"""
|
|
Manages a collection of entries that represent the current state of the system.
|
|
|
|
The working memory stores different types of entries (scripts, reasoning, errors, etc.)
|
|
and provides methods to add, remove, and update them. It also generates XML context
|
|
representing the current state.
|
|
|
|
Notifies observers of entry additions, deletions and changes. During update(),
|
|
changes are batched and a single notification is sent after completion.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize an empty working memory."""
|
|
self._entries: List[Entry] = []
|
|
self._change_handlers: List[Callable[[], None]] = []
|
|
self._updating = False
|
|
self._changed_during_update: Set[Entry] = set()
|
|
|
|
def add_change_handler(self, handler: Callable[[], None]) -> None:
|
|
"""Add a callback for working memory changes."""
|
|
if handler not in self._change_handlers:
|
|
self._change_handlers.append(handler)
|
|
|
|
def _notify_change(self) -> None:
|
|
"""Notify all handlers of working memory changes."""
|
|
self._sort_entries()
|
|
for handler in self._change_handlers:
|
|
handler()
|
|
|
|
def _handle_entry_change(self, entry: Entry) -> None:
|
|
"""Handle changes from individual entries."""
|
|
if self._updating:
|
|
self._changed_during_update.add(entry)
|
|
else:
|
|
self._notify_change()
|
|
|
|
def _sort_entries(self) -> None:
|
|
"""Sort entries by ID in chronological order."""
|
|
self._entries.sort(key=lambda x: x.id)
|
|
|
|
def add_entry(self, entry: Entry) -> None:
|
|
"""
|
|
Add a new entry to working memory.
|
|
|
|
Args:
|
|
entry: Entry object to add
|
|
"""
|
|
if not isinstance(entry, Entry):
|
|
raise TypeError("Entry must be an instance of Entry class")
|
|
entry.add_change_handler(self._handle_entry_change)
|
|
self._entries.append(entry)
|
|
self._sort_entries()
|
|
self._notify_change()
|
|
|
|
def remove_entry(self, id: str) -> None:
|
|
"""
|
|
Remove an entry from working memory by its ID.
|
|
Ensures cleanup is performed before removal.
|
|
|
|
Args:
|
|
id: Unique identifier of entry to remove
|
|
"""
|
|
entry = self.get_entry(id)
|
|
if entry is not None:
|
|
entry.cleanup()
|
|
self._entries = [e for e in self._entries if e.id != id]
|
|
self._sort_entries()
|
|
self._notify_change()
|
|
|
|
def clear(self) -> None:
|
|
"""
|
|
Remove all entries from working memory.
|
|
Ensures cleanup is performed on all entries.
|
|
"""
|
|
for entry in self._entries:
|
|
entry.cleanup()
|
|
self._entries.clear()
|
|
self._notify_change()
|
|
|
|
def __del__(self):
|
|
"""Clean up all entries when memory is deleted."""
|
|
if hasattr(self, '_entries'):
|
|
self.clear()
|
|
|
|
def get_entry(self, id: str) -> Optional[Entry]:
|
|
"""
|
|
Get an entry by its ID.
|
|
|
|
Args:
|
|
id: Unique identifier of entry to retrieve
|
|
|
|
Returns:
|
|
Entry if found, None otherwise
|
|
"""
|
|
for entry in self._entries:
|
|
if entry.id == id:
|
|
return entry
|
|
return None
|
|
|
|
def get_entries(self) -> List[Entry]:
|
|
"""
|
|
Get all entries in working memory.
|
|
|
|
Returns:
|
|
List[Entry]: List of all entries in chronological order
|
|
"""
|
|
return self._entries.copy()
|
|
|
|
def get_entries_count(self) -> int:
|
|
"""
|
|
Get the total number of entries.
|
|
|
|
Returns:
|
|
int: Number of entries in working memory
|
|
"""
|
|
return len(self._entries)
|
|
|
|
def update(self) -> None:
|
|
"""
|
|
Update all entries in working memory.
|
|
Batches change notifications and sends single update after completion.
|
|
"""
|
|
self._updating = True
|
|
self._changed_during_update.clear()
|
|
|
|
for entry in self._entries:
|
|
entry.update()
|
|
|
|
self._updating = False
|
|
if self._changed_during_update:
|
|
self._notify_change()
|
|
|
|
def generate_context(self) -> List[ET.Element]:
|
|
"""
|
|
Generate XML Elements representing all entries.
|
|
|
|
Returns:
|
|
List[ET.Element]: List of XML elements for each entry
|
|
"""
|
|
return [entry.generate_context() for entry in self._entries]
|
|
|
|
===== FILE: sia/xml_validator.py =====
|
|
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()
|
|
===== FILE: tools/itb/requirements.txt =====
|
|
selenium>=4.0.0
|
|
webdriver-manager>=3.8.0
|
|
click>=8.0.0
|
|
beautifulsoup4>=4.9.0
|
|
pytest>=7.0.0
|
|
pytest-cov>=4.0.0
|
|
black>=22.0.0
|
|
flake8>=4.0.0
|
|
|
|
===== FILE: tools/itb/setup.py =====
|
|
from setuptools import setup, find_packages
|
|
|
|
setup(
|
|
name="itb",
|
|
version="0.1.0",
|
|
packages=find_packages(),
|
|
scripts=[
|
|
'bin/itb_click',
|
|
'bin/itb_cursor',
|
|
'bin/itb_input',
|
|
'bin/itb_navigate',
|
|
'bin/itb_refresh',
|
|
'bin/itb_screenshot',
|
|
'bin/itb_scroll',
|
|
'bin/itb_start'
|
|
],
|
|
install_requires=[
|
|
'selenium>=4.0.0',
|
|
'webdriver-manager>=3.8.0',
|
|
'click>=8.0.0',
|
|
'beautifulsoup4>=4.9.0',
|
|
'pytest>=7.0.0',
|
|
'pytest-cov>=4.0.0',
|
|
'black>=22.0.0',
|
|
'flake8>=4.0.0'
|
|
],
|
|
classifiers=[
|
|
'Development Status :: 3 - Alpha',
|
|
'Intended Audience :: Developers',
|
|
'Programming Language :: Python :: 3',
|
|
'Programming Language :: Python :: 3.10',
|
|
],
|
|
python_requires='>=3.10',
|
|
)
|
|
===== FILE: tools/train/requirements.txt =====
|
|
pyyaml>=6.0
|
|
requests>=2.28.0
|
|
torch>=2.0.0
|
|
transformers>=4.30.0
|
|
# DeepSeek support
|
|
accelerate>=0.25.0
|
|
bitsandbytes>=0.41.1
|
|
einops>=0.7.0
|
|
sentencepiece>=0.1.99
|
|
unsloth>=2024.3
|
|
trl>=0.7.8
|
|
datasets>=2.14.6
|
|
peft>=0.8.0
|
|
===== FILE: tools/train/setup.py =====
|
|
from setuptools import setup, find_packages
|
|
|
|
setup(
|
|
name="train",
|
|
version="0.1.0",
|
|
packages=find_packages(),
|
|
scripts=[
|
|
'bin/train_deepseek',
|
|
'bin/train_mistral'
|
|
],
|
|
install_requires=[
|
|
'pyyaml>=6.0',
|
|
'requests>=2.28.0',
|
|
'torch>=2.0.0',
|
|
'transformers>=4.30.0',
|
|
'accelerate>=0.25.0',
|
|
'bitsandbytes>=0.41.1',
|
|
'einops>=0.7.0',
|
|
'sentencepiece>=0.1.99',
|
|
'unsloth>=2024.3',
|
|
'trl>=0.7.8',
|
|
'datasets>=2.14.6',
|
|
'peft>=0.8.0',
|
|
'pytest>=7.0.0',
|
|
'pytest-cov>=4.0.0',
|
|
'black>=22.0.0',
|
|
'flake8>=4.0.0'
|
|
],
|
|
classifiers=[
|
|
'Development Status :: 3 - Alpha',
|
|
'Intended Audience :: Developers',
|
|
'Programming Language :: Python :: 3',
|
|
'Programming Language :: Python :: 3.10',
|
|
],
|
|
python_requires='>=3.10',
|
|
)
|
|
===== FILE: tools/train/train.sh =====
|
|
#!/bin/bash
|
|
|
|
set -e
|
|
|
|
SIA_DIR="/root/sia"
|
|
OUTPUT_DIR="${1:-/root/models/$(cd "$SIA_DIR" && git rev-parse HEAD)}"
|
|
|
|
if [ -n "$(cd "$SIA_DIR" && git status --porcelain)" ]; then
|
|
echo "Uncommitted changes in SIA directory"
|
|
#exit 1
|
|
fi
|
|
|
|
mkdir -p "$OUTPUT_DIR"
|
|
|
|
train_deepseek --output-dir "$OUTPUT_DIR"
|
|
===== FILE: tools/train/train/__init__.py =====
|
|
"""
|
|
SIA Training Tool
|
|
|
|
This package provides utilities for fine-tuning language models used by SIA.
|
|
Supports DeepSeek and Mistral models.
|
|
"""
|
|
|
|
__version__ = "0.1.0"
|
|
===== FILE: tools/train/train/mistral_api.py =====
|
|
#!/root/venvs/train/bin/python
|
|
"""
|
|
Script for fine-tuning Mistral models for SIA using the Mistral API.
|
|
"""
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import requests
|
|
|
|
# Import from our shared library
|
|
from .util import TrainingParams, DatasetCreator
|
|
|
|
@dataclass
|
|
class Config:
|
|
def __init__(self):
|
|
parser = argparse.ArgumentParser(description='Train SIA model using Mistral API')
|
|
parser.add_argument(
|
|
'--config',
|
|
type=Path,
|
|
default=Path('/root/sia/training/config.yaml'),
|
|
help='Path to config file'
|
|
)
|
|
parser.add_argument(
|
|
'--model',
|
|
type=str,
|
|
default='mistral-large-latest',
|
|
help='Base model for fine-tuning'
|
|
)
|
|
parser.add_argument(
|
|
'--api-key',
|
|
type=str,
|
|
default=os.environ.get('SIA_MISTRAL_API_KEY'),
|
|
help='Mistral API key'
|
|
)
|
|
self.args = parser.parse_args()
|
|
|
|
@property
|
|
def config_path(self) -> Path:
|
|
return self.args.config
|
|
|
|
@property
|
|
def model(self) -> str:
|
|
return self.args.model
|
|
|
|
@property
|
|
def api_key(self) -> str:
|
|
return self.args.api_key
|
|
|
|
def upload_file(api_key: str, file_path: Path) -> str:
|
|
"""Upload a file to the Mistral API and return the file ID"""
|
|
url = "https://api.mistral.ai/v1/files"
|
|
headers = {
|
|
"Authorization": f"Bearer {api_key}"
|
|
}
|
|
files = {
|
|
"file": ("dataset.jsonl", open(file_path, "rb"), "application/jsonl"),
|
|
"purpose": (None, "fine-tune")
|
|
}
|
|
|
|
response = requests.post(url, headers=headers, files=files)
|
|
if response.status_code != 200:
|
|
print(f"Error uploading file: {response.text}")
|
|
sys.exit(1)
|
|
|
|
return response.json()["id"]
|
|
|
|
def start_finetune_job(api_key: str, model: str, file_id: str, params: sia_train_lib.TrainingParams):
|
|
"""Start a fine-tuning job on the Mistral API"""
|
|
headers = {
|
|
"Authorization": f"Bearer {api_key}",
|
|
"Content-Type": "application/json"
|
|
}
|
|
data = {
|
|
"model": model,
|
|
"training_files": [{"file_id": file_id, "weight": 1}],
|
|
"hyperparameters": {
|
|
"learning_rate": params.learning_rate,
|
|
"epochs": params.epochs
|
|
}
|
|
}
|
|
|
|
response = requests.post(
|
|
"https://api.mistral.ai/v1/fine_tuning/jobs",
|
|
headers=headers,
|
|
json=data
|
|
)
|
|
|
|
if response.status_code != 200:
|
|
print(f"Error creating fine-tuning job: {response.text}")
|
|
return None
|
|
|
|
return response.json()["id"]
|
|
|
|
def main():
|
|
config = Config()
|
|
if not config.api_key:
|
|
print("Error: Mistral API key not found. Set SIA_MISTRAL_API_KEY environment variable.")
|
|
return 1
|
|
|
|
training_data, train_params, commit_hash = sia_train_lib.prepare_training_data(config.config_path)
|
|
|
|
if not training_data:
|
|
print("No valid training data found. Exiting.")
|
|
return 1
|
|
|
|
model_name = f"sia_{commit_hash}"
|
|
|
|
# Create temp file and upload
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.jsonl', delete=False) as f:
|
|
for sample in training_data:
|
|
json.dump(sample, f, ensure_ascii=False)
|
|
f.write('\n')
|
|
|
|
try:
|
|
file_id = upload_file(config.api_key, Path(f.name))
|
|
|
|
# Start fine-tuning job
|
|
job_id = start_finetune_job(
|
|
api_key=config.api_key,
|
|
model=config.model,
|
|
file_id=file_id,
|
|
params=train_params
|
|
)
|
|
|
|
if not job_id:
|
|
return 1
|
|
|
|
print(f"Started fine-tuning job: {model_name}")
|
|
print(f"Job ID: {job_id}")
|
|
print(f"Check status: curl -H 'Authorization: Bearer {config.api_key}' https://api.mistral.ai/v1/fine_tuning/jobs/{job_id}")
|
|
finally:
|
|
os.unlink(f.name)
|
|
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
exit(main())
|
|
|
|
===== FILE: tools/train/train/unsloth_deepseek.py =====
|
|
#!/root/venvs/train/bin/python
|
|
"""
|
|
Script for fine-tuning DeepSeek models for SIA using Unsloth.
|
|
Training always starts from a base model and creates a new fine-tuned model.
|
|
"""
|
|
import argparse
|
|
import os
|
|
import sys
|
|
import torch
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
import json
|
|
|
|
# Import from shared library
|
|
from .util import prepare_training_data
|
|
|
|
@dataclass
|
|
class Config:
|
|
def __init__(self):
|
|
parser = argparse.ArgumentParser(description='Train SIA model using Unsloth')
|
|
parser.add_argument(
|
|
'--config',
|
|
type=Path,
|
|
default=Path('/root/sia/training/config.yaml'),
|
|
help='Path to config file'
|
|
)
|
|
parser.add_argument(
|
|
'--base-model',
|
|
type=str,
|
|
default='deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B',
|
|
help='HuggingFace model ID for base model'
|
|
)
|
|
parser.add_argument(
|
|
'--output-dir',
|
|
type=Path,
|
|
required=True,
|
|
help='Directory to save the trained model'
|
|
)
|
|
parser.add_argument(
|
|
'--api-key',
|
|
type=str,
|
|
default=os.environ.get('SIA_HF_API_KEY'),
|
|
help='HuggingFace API key'
|
|
)
|
|
self.args = parser.parse_args()
|
|
|
|
@property
|
|
def config_path(self) -> Path:
|
|
return self.args.config
|
|
|
|
@property
|
|
def base_model(self) -> str:
|
|
return self.args.base_model
|
|
|
|
@property
|
|
def output_dir(self) -> Path:
|
|
return self.args.output_dir
|
|
|
|
@property
|
|
def api_key(self) -> str:
|
|
return self.args.api_key
|
|
|
|
def train_model(config: Config, training_data, train_params, commit_hash):
|
|
"""Train the model using Unsloth"""
|
|
try:
|
|
from unsloth import FastLanguageModel
|
|
from transformers import TrainingArguments, DataCollatorForSeq2Seq
|
|
from trl import SFTTrainer
|
|
from datasets import Dataset
|
|
from unsloth.chat_templates import get_chat_template, train_on_responses_only
|
|
except ImportError as e:
|
|
print(f"Error importing required libraries: {e}")
|
|
print("Please ensure Unsloth and its dependencies are installed.")
|
|
sys.exit(1)
|
|
|
|
print(f"Starting training from base model: {config.base_model}")
|
|
|
|
# Convert to datasets format
|
|
dataset = Dataset.from_list(training_data)
|
|
|
|
# Determine if bfloat16 is supported
|
|
dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
|
|
|
|
# Load the model - always from a base model (no incremental updates)
|
|
try:
|
|
model, tokenizer = FastLanguageModel.from_pretrained(
|
|
model_name=config.base_model,
|
|
max_seq_length=2048,
|
|
dtype=dtype,
|
|
load_in_4bit=True,
|
|
token=config.api_key,
|
|
)
|
|
except Exception as e:
|
|
print(f"Error loading base model: {e}")
|
|
sys.exit(1)
|
|
|
|
# Apply LoRA
|
|
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
|
|
model = FastLanguageModel.get_peft_model(
|
|
model,
|
|
r=16,
|
|
target_modules=target_modules,
|
|
lora_alpha=16,
|
|
lora_dropout=0,
|
|
bias="none",
|
|
use_gradient_checkpointing="unsloth",
|
|
random_state=3407,
|
|
)
|
|
|
|
# Apply chat template
|
|
tokenizer = get_chat_template(
|
|
tokenizer,
|
|
chat_template="llama-3.1", # Compatible with DeepSeek
|
|
)
|
|
|
|
# Function to format conversations
|
|
def formatting_prompts_func(examples):
|
|
convos = examples["conversations"]
|
|
texts = [tokenizer.apply_chat_template(convo, tokenize=False, add_generation_prompt=False) for convo in convos]
|
|
return {"text": texts}
|
|
|
|
# Standarize dataset and format
|
|
from unsloth.chat_templates import standardize_sharegpt
|
|
|
|
# Add conversations field if not present
|
|
if "conversations" not in dataset.column_names:
|
|
if "messages" in dataset.column_names:
|
|
dataset = dataset.rename_column("messages", "conversations")
|
|
else:
|
|
dataset = dataset.map(lambda x: {"conversations": [{"role": "system", "content": x.get("system_prompt", "")},
|
|
{"role": "user", "content": x.get("prompt", "")},
|
|
{"role": "assistant", "content": x.get("response", "")}]})
|
|
|
|
# Standardize format
|
|
dataset = standardize_sharegpt(dataset)
|
|
|
|
# Apply formatting
|
|
dataset = dataset.map(formatting_prompts_func, batched=True)
|
|
|
|
# Configure the trainer
|
|
output_dir = config.output_dir / commit_hash
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Determine steps or epochs based on dataset size
|
|
max_steps = None
|
|
num_train_epochs = train_params.epochs
|
|
if len(dataset) < 100: # Small dataset
|
|
# Aim for at least 500 steps for small datasets
|
|
max_steps = 500
|
|
num_train_epochs = None
|
|
|
|
trainer = SFTTrainer(
|
|
model=model,
|
|
tokenizer=tokenizer,
|
|
train_dataset=dataset,
|
|
dataset_text_field="text",
|
|
max_seq_length=2048,
|
|
data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer),
|
|
dataset_num_proc=2,
|
|
packing=False,
|
|
args=TrainingArguments(
|
|
per_device_train_batch_size=2,
|
|
gradient_accumulation_steps=4,
|
|
warmup_steps=5,
|
|
max_steps=max_steps,
|
|
num_train_epochs=num_train_epochs,
|
|
learning_rate=train_params.learning_rate,
|
|
fp16=not torch.cuda.is_bf16_supported(),
|
|
bf16=torch.cuda.is_bf16_supported(),
|
|
logging_steps=10,
|
|
optim="adamw_8bit",
|
|
weight_decay=0.01,
|
|
lr_scheduler_type="linear",
|
|
seed=3407,
|
|
output_dir=str(output_dir),
|
|
report_to="none",
|
|
),
|
|
)
|
|
|
|
# Train only on responses
|
|
trainer = train_on_responses_only(
|
|
trainer,
|
|
instruction_part="<|start_header_id|>user<|end_header_id|>\n\n",
|
|
response_part="<|start_header_id|>assistant<|end_header_id|>\n\n",
|
|
)
|
|
|
|
# Train the model
|
|
trainer.train()
|
|
|
|
# Enable inference mode for the model
|
|
model = FastLanguageModel.for_inference(model)
|
|
|
|
# Save the model
|
|
model.save_pretrained(output_dir)
|
|
tokenizer.save_pretrained(output_dir)
|
|
|
|
# Create a metadata file with training information
|
|
with open(output_dir / "training_info.json", "w") as f:
|
|
json.dump({
|
|
"base_model": config.base_model,
|
|
"commit_hash": commit_hash,
|
|
"learning_rate": train_params.learning_rate,
|
|
"epochs": train_params.epochs,
|
|
"dataset_size": len(dataset),
|
|
"training_method": "unsloth",
|
|
}, f, indent=2)
|
|
|
|
return output_dir
|
|
|
|
def main():
|
|
config = Config()
|
|
|
|
# Prepare training data
|
|
training_data, train_params, commit_hash = prepare_training_data(config.config_path)
|
|
|
|
if not training_data:
|
|
print("No valid training data found. Exiting.")
|
|
return 1
|
|
|
|
# Train the model
|
|
try:
|
|
model_dir = train_model(config, training_data, train_params, commit_hash)
|
|
|
|
# Create symlink to current
|
|
current_link = config.output_dir / "current"
|
|
if current_link.exists() or current_link.is_symlink():
|
|
current_link.unlink()
|
|
os.symlink(model_dir, current_link, target_is_directory=True)
|
|
|
|
print(f"Training complete. Model saved to {model_dir}")
|
|
print(f"Symlink created at {current_link}")
|
|
|
|
return 0
|
|
except Exception as e:
|
|
print(f"Error during training: {e}")
|
|
return 1
|
|
|
|
if __name__ == "__main__":
|
|
exit(main())
|
|
===== FILE: tools/train/train/util.py =====
|
|
"""
|
|
Shared library for SIA model training functionality.
|
|
Contains common code for both API-based and local training.
|
|
"""
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Set, Tuple
|
|
import hashlib
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
import yaml
|
|
|
|
@dataclass
|
|
class TrainingParams:
|
|
"""Parameters for model training"""
|
|
learning_rate: float
|
|
epochs: int
|
|
batch_size: int = 1
|
|
|
|
class DatasetCreator:
|
|
"""Creates training datasets from XML iteration files"""
|
|
|
|
def __init__(
|
|
self,
|
|
xml_files: Set[Path],
|
|
system_prompt_file: Path,
|
|
action_schema_file: Path
|
|
):
|
|
self.xml_files = xml_files
|
|
self.system_prompt_file = Path(system_prompt_file)
|
|
self.action_schema_file = Path(action_schema_file)
|
|
|
|
self.system_prompt = self.system_prompt_file.read_text()
|
|
self.system_prompt_hash = self._calculate_hash(self.system_prompt)
|
|
|
|
self.action_schema = self.action_schema_file.read_text()
|
|
self.action_schema_hash = self._calculate_hash(self.action_schema)
|
|
|
|
def _calculate_hash(self, content: str) -> str:
|
|
"""Calculate SHA-256 hash of content"""
|
|
return hashlib.sha256(content.encode()).hexdigest()
|
|
|
|
def _parse_iteration_file(self, file_path: Path) -> Optional[Dict]:
|
|
"""Parse a single iteration XML file into a training example"""
|
|
try:
|
|
tree = ET.parse(file_path)
|
|
root = tree.getroot()
|
|
|
|
# Check hashes to ensure compatibility
|
|
if root.get('system_prompt_hash') != self.system_prompt_hash:
|
|
print(f"System prompt hash mismatch in {file_path}")
|
|
return None
|
|
if root.get('action_schema_hash') != self.action_schema_hash:
|
|
print(f"Action schema hash mismatch in {file_path}")
|
|
return None
|
|
|
|
context_elem = root.find('context')
|
|
response_elem = root.find('response')
|
|
|
|
if context_elem is None or response_elem is None:
|
|
print(f"Missing context or response elements in {file_path}")
|
|
return None
|
|
|
|
context = context_elem.text
|
|
response = response_elem.text
|
|
|
|
if not context or not response:
|
|
print(f"Empty context or response in {file_path}")
|
|
return None
|
|
|
|
return {
|
|
"messages": [
|
|
{
|
|
"role": "system",
|
|
"content": self.system_prompt + "\n" + self.action_schema
|
|
},
|
|
{
|
|
"role": "user",
|
|
"content": context
|
|
},
|
|
{
|
|
"role": "assistant",
|
|
"content": response
|
|
}
|
|
]
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f"Error processing {file_path}: {str(e)}")
|
|
return None
|
|
|
|
def create_dataset(self) -> List[Dict]:
|
|
"""Create a dataset from all valid XML files"""
|
|
samples = []
|
|
total_files = len(self.xml_files)
|
|
print(f"Processing {total_files} XML files...")
|
|
|
|
for i, xml_file in enumerate(sorted(self.xml_files)):
|
|
if i % 10 == 0:
|
|
print(f"Processed {i}/{total_files} files...")
|
|
|
|
sample = self._parse_iteration_file(xml_file)
|
|
if sample:
|
|
samples.append(sample)
|
|
|
|
print(f"Created dataset with {len(samples)} samples from {total_files} files")
|
|
return samples
|
|
|
|
def find_xml_files(data_paths: List[Path]) -> Set[Path]:
|
|
"""Find all XML files in the given data paths"""
|
|
xml_files = set()
|
|
for path in data_paths:
|
|
if not path.exists():
|
|
print(f"Error: Data path not found: {path}")
|
|
sys.exit(1)
|
|
xml_files.update(path.rglob('*.xml'))
|
|
return xml_files
|
|
|
|
def format_chat_for_mistral(messages):
|
|
"""Format messages for Mistral chat format"""
|
|
# Mistral uses a specific chat format:
|
|
# <s>[INST] {system + user content} [/INST] {assistant response} </s>
|
|
|
|
system_content = ""
|
|
user_content = ""
|
|
assistant_content = ""
|
|
|
|
for msg in messages:
|
|
role = msg["role"]
|
|
content = msg["content"]
|
|
|
|
if role == "system":
|
|
system_content = content
|
|
elif role == "user":
|
|
user_content = content
|
|
elif role == "assistant":
|
|
assistant_content = content
|
|
|
|
# Combine system and user content for the instruction
|
|
instruction = system_content
|
|
if instruction and user_content:
|
|
instruction += "\n\n"
|
|
instruction += user_content
|
|
|
|
# Format according to Mistral chat template
|
|
return f"<s>[INST] {instruction} [/INST] {assistant_content} </s>"
|
|
|
|
def prepare_training_data(config_path: Path) -> Tuple[List[Dict], TrainingParams, str]:
|
|
"""Prepare training data from config and XML files"""
|
|
with open(config_path) as f:
|
|
config_data = yaml.safe_load(f)
|
|
|
|
data_paths = [Path(p) for p in config_data['data']]
|
|
xml_files = find_xml_files(data_paths)
|
|
|
|
paths = list(xml_files)
|
|
paths.append(config_path)
|
|
paths.append(Path(config_data['model']['system_prompt_path']))
|
|
paths.append(Path(config_data['model']['action_schema']))
|
|
commit_hash = check_git_status(paths)
|
|
|
|
creator = DatasetCreator(
|
|
xml_files=xml_files,
|
|
system_prompt_file=config_data['model']['system_prompt_path'],
|
|
action_schema_file=config_data['model']['action_schema']
|
|
)
|
|
|
|
training_data = creator.create_dataset()
|
|
|
|
train_params = TrainingParams(
|
|
learning_rate=config_data['params'].get('learning_rate', 1e-5),
|
|
epochs=config_data['params'].get('epochs', 3),
|
|
batch_size=config_data['params'].get('batch_size', 1)
|
|
)
|
|
|
|
return training_data, train_params, commit_hash
|
|
|
|
def save_jsonl_dataset(data: List[Dict], output_path: Path) -> None:
|
|
"""Save dataset in JSONL format"""
|
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|
for sample in data:
|
|
json.dump(sample, f, ensure_ascii=False)
|
|
f.write('\n')
|
|
print(f"Saved dataset with {len(data)} samples to {output_path}")
|
|
|
|
===== FILE: training/config.yaml =====
|
|
model:
|
|
system_prompt_path: "/root/sia/system_prompt.md"
|
|
action_schema: "/root/sia/action_schema.xsd"
|
|
params:
|
|
learning_rate: 1e-5
|
|
epochs: 3
|
|
data:
|
|
- "/root/sia/training/clean_start/"
|
|
- "/root/sia/training/delete_indicated_entries/"
|
|
- "/root/sia/training/list_entries_to_delete/" |