feat: Add synopisis core loop (#166)
Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from exchange import Exchange, Message, ToolResult, ToolUse
|
||||
from exchange import Message, ToolResult, ToolUse
|
||||
from goose.cli.prompt.goose_prompt_session import GoosePromptSession
|
||||
from goose.cli.prompt.user_input import PromptAction, UserInput
|
||||
from goose.cli.session import Session
|
||||
@@ -124,63 +123,6 @@ def test_log_log_cost(create_session_with_mock_configs):
|
||||
mock_logger.info.assert_called_once_with(cost_message)
|
||||
|
||||
|
||||
@patch.object(GoosePromptSession, "get_user_input", name="get_user_input")
|
||||
@patch.object(Exchange, "generate", name="mock_generate")
|
||||
@patch("goose.cli.session.save_latest_session", name="mock_save_latest_session")
|
||||
def test_run_should_auto_save_session(
|
||||
mock_save_latest_session,
|
||||
mock_generate,
|
||||
mock_get_user_input,
|
||||
create_session_with_mock_configs,
|
||||
mock_sessions_path,
|
||||
):
|
||||
def custom_exchange_generate(self, *args, **kwargs):
|
||||
message = Message.assistant("Response")
|
||||
self.add(message)
|
||||
return message
|
||||
|
||||
def mock_generate_side_effect(*args, **kwargs):
|
||||
return custom_exchange_generate(session.exchange, *args, **kwargs)
|
||||
|
||||
def save_latest_session(file, messages):
|
||||
file.write_text("\n".join(json.dumps(m.to_dict()) for m in messages))
|
||||
|
||||
user_inputs = [
|
||||
UserInput(action=PromptAction.CONTINUE, text="Question1"),
|
||||
UserInput(action=PromptAction.CONTINUE, text="Question2"),
|
||||
UserInput(action=PromptAction.EXIT),
|
||||
]
|
||||
session = create_session_with_mock_configs({"name": SESSION_NAME})
|
||||
|
||||
mock_get_user_input.side_effect = user_inputs
|
||||
mock_generate.side_effect = mock_generate_side_effect
|
||||
mock_save_latest_session.side_effect = save_latest_session
|
||||
|
||||
session.run()
|
||||
|
||||
session_file = mock_sessions_path / f"{SESSION_NAME}.jsonl"
|
||||
|
||||
assert mock_generate.call_count == 2
|
||||
assert mock_save_latest_session.call_count == 2
|
||||
assert mock_save_latest_session.call_args_list[0][0][0] == session_file
|
||||
assert session_file.exists()
|
||||
|
||||
with open(session_file, "r") as f:
|
||||
saved_messages = [json.loads(line) for line in f]
|
||||
|
||||
expected_messages = [
|
||||
Message.user("Question1"),
|
||||
Message.assistant("Response"),
|
||||
Message.user("Question2"),
|
||||
Message.assistant("Response"),
|
||||
]
|
||||
|
||||
assert len(saved_messages) == len(expected_messages)
|
||||
for saved, expected in zip(saved_messages, expected_messages):
|
||||
assert saved["role"] == expected.role
|
||||
assert saved["content"][0]["text"] == expected.text
|
||||
|
||||
|
||||
@patch("goose.cli.session.droid", return_value="generated_session_name", name="mock_droid")
|
||||
def test_set_generated_session_name(mock_droid, create_session_with_mock_configs, mock_sessions_path):
|
||||
session = create_session_with_mock_configs({"name": None})
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from exchange.content import Text
|
||||
from exchange.message import Message
|
||||
from goose.synopsis.moderator import Synopsis
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_exchange(exchange_factory):
|
||||
exchange = exchange_factory()
|
||||
exchange.messages.extend(
|
||||
[
|
||||
Message(role="user", content=[Text("Test content for user message")]),
|
||||
Message(role="assistant", content=[Text("Test content for assistant message")]),
|
||||
Message(role="user", content=[Text("Another user message")]),
|
||||
]
|
||||
)
|
||||
return exchange
|
||||
|
||||
|
||||
def test_rewrite_with_tool_use(mock_exchange):
|
||||
tool_use_message = Message(role="user", content=[Text("Tool use message")])
|
||||
mock_exchange.messages.append(tool_use_message)
|
||||
|
||||
with patch.object(Synopsis, "get_synopsis") as mock_get_synopsis:
|
||||
message = Message(role="synopsis", content=[Text("Updated synopsis")])
|
||||
mock_get_synopsis.return_value = message
|
||||
synopsis = Synopsis()
|
||||
synopsis.rewrite(mock_exchange)
|
||||
|
||||
# The first message should be replaced, and the rest are cleared
|
||||
assert mock_exchange.messages == [message]
|
||||
@@ -0,0 +1,62 @@
|
||||
import pytest
|
||||
import time
|
||||
import requests
|
||||
from goose.synopsis.toolkit import SynopsisDeveloper
|
||||
from goose.synopsis.system import system
|
||||
|
||||
|
||||
class MockNotifier:
|
||||
def log(self, message):
|
||||
pass
|
||||
|
||||
def status(self, message):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def toolkit(tmpdir):
|
||||
original_cwd = system.cwd
|
||||
system.cwd = str(tmpdir)
|
||||
notifier = MockNotifier()
|
||||
toolkit = SynopsisDeveloper(notifier=notifier)
|
||||
|
||||
yield toolkit
|
||||
|
||||
# Teardown: cancel all processes and restore original working directory
|
||||
for process_id in list(system._processes.keys()):
|
||||
system.cancel_process(process_id)
|
||||
system.cwd = original_cwd
|
||||
|
||||
|
||||
def test_start_process(toolkit):
|
||||
process_id = toolkit.start_process("python -m http.server 8000")
|
||||
assert process_id > 0
|
||||
time.sleep(2) # Give the server time to start
|
||||
|
||||
# Check if the server is running
|
||||
try:
|
||||
response = requests.get("http://localhost:8000")
|
||||
assert response.status_code == 200
|
||||
except requests.ConnectionError:
|
||||
pytest.fail("HTTP server did not start successfully")
|
||||
output = toolkit.view_process_output(process_id)
|
||||
assert "200" in output
|
||||
|
||||
|
||||
def test_list_processes(toolkit):
|
||||
process_id = toolkit.start_process("python -m http.server 8001")
|
||||
processes = toolkit.list_processes()
|
||||
assert process_id in processes
|
||||
assert "python -m http.server 8001" in processes[process_id]
|
||||
|
||||
|
||||
def test_cancel_process(toolkit):
|
||||
process_id = toolkit.start_process("python -m http.server 8003")
|
||||
time.sleep(2) # Give the server time to start
|
||||
|
||||
result = toolkit.cancel_process(process_id)
|
||||
assert result == f"process {process_id} cancelled"
|
||||
|
||||
# Verify that the process is no longer running
|
||||
with pytest.raises(ValueError):
|
||||
toolkit.view_process_output(process_id)
|
||||
@@ -0,0 +1,93 @@
|
||||
import os
|
||||
from unittest.mock import Mock
|
||||
import pytest
|
||||
from goose.synopsis.system import OperatingSystem
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def os_instance(tmpdir):
|
||||
original_cwd = os.getcwd()
|
||||
os.chdir(tmpdir)
|
||||
yield OperatingSystem(cwd=str(tmpdir))
|
||||
os.chdir(original_cwd)
|
||||
|
||||
|
||||
def test_to_relative(os_instance, tmpdir):
|
||||
abs_path = os.path.join(tmpdir, "test_file.txt")
|
||||
rel_path = os_instance.to_relative(abs_path)
|
||||
assert rel_path == "test_file.txt"
|
||||
|
||||
|
||||
def test_remember_forget_file(os_instance, tmpdir):
|
||||
test_file = tmpdir.join("test_file.txt")
|
||||
test_file.write("test content")
|
||||
|
||||
os_instance.remember_file(str(test_file))
|
||||
assert os_instance.is_active(str(test_file))
|
||||
|
||||
os_instance.forget_file(str(test_file))
|
||||
assert not os_instance.is_active(str(test_file))
|
||||
|
||||
|
||||
def test_active_files(os_instance, tmpdir):
|
||||
test_file1 = tmpdir.join("test_file1.txt")
|
||||
test_file2 = tmpdir.join("test_file2.py")
|
||||
test_file1.write("test content 1")
|
||||
test_file2.write("test content 2")
|
||||
|
||||
os_instance.remember_file(str(test_file1))
|
||||
os_instance.remember_file(str(test_file2))
|
||||
|
||||
active_files = list(os_instance.active_files)
|
||||
assert len(active_files) == 2
|
||||
assert any(f.path == "test_file1.txt" for f in active_files)
|
||||
assert any(f.path == "test_file2.py" for f in active_files)
|
||||
|
||||
|
||||
def test_info(os_instance):
|
||||
info = os_instance.info()
|
||||
assert "os" in info
|
||||
assert "cwd" in info
|
||||
assert "shell" in info
|
||||
|
||||
|
||||
def test_add_process(os_instance):
|
||||
process = Mock()
|
||||
process.pid = 1234
|
||||
process.stdout = Mock()
|
||||
process.stdout.fileno.return_value = 1
|
||||
process_id = os_instance.add_process(process)
|
||||
assert process_id == 1234
|
||||
assert 1234 in os_instance._processes
|
||||
|
||||
|
||||
def test_get_processes(os_instance):
|
||||
process1 = Mock()
|
||||
process1.pid = 1234
|
||||
process1.args = "python -m http.server 8000"
|
||||
process1.stdout = Mock()
|
||||
process1.stdout.fileno.return_value = 1
|
||||
os_instance.add_process(process1)
|
||||
|
||||
process2 = Mock()
|
||||
process2.pid = 5678
|
||||
process2.args = "python script.py"
|
||||
process2.stdout = Mock()
|
||||
process2.stdout.fileno.return_value = 2
|
||||
os_instance.add_process(process2)
|
||||
|
||||
processes = os_instance.get_processes()
|
||||
assert processes == {1234: "python -m http.server 8000", 5678: "python script.py"}
|
||||
|
||||
|
||||
def test_cancel_process(os_instance):
|
||||
process = Mock()
|
||||
process.pid = 1234
|
||||
process.stdout = Mock()
|
||||
process.stdout.fileno.return_value = 1
|
||||
os_instance.add_process(process)
|
||||
|
||||
result = os_instance.cancel_process(1234)
|
||||
assert result is True
|
||||
assert 1234 not in os_instance._processes
|
||||
process.terminate.assert_called_once()
|
||||
@@ -0,0 +1,95 @@
|
||||
import os
|
||||
import pytest
|
||||
from goose.synopsis.toolkit import SynopsisDeveloper
|
||||
from goose.synopsis.system import system
|
||||
|
||||
|
||||
class MockNotifier:
|
||||
def log(self, message):
|
||||
pass
|
||||
|
||||
def status(self, message):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def toolkit(tmpdir):
|
||||
original_cwd = os.getcwd()
|
||||
os.chdir(tmpdir)
|
||||
system.cwd = str(tmpdir)
|
||||
notifier = MockNotifier()
|
||||
toolkit = SynopsisDeveloper(notifier=notifier)
|
||||
|
||||
yield toolkit
|
||||
|
||||
# Teardown: cancel all processes and restore original working directory
|
||||
for process_id in list(system._processes.keys()):
|
||||
system.cancel_process(process_id)
|
||||
os.chdir(original_cwd)
|
||||
system.cwd = original_cwd
|
||||
|
||||
|
||||
def test_shell(toolkit, tmpdir):
|
||||
result = toolkit.shell("echo 'Hello, World!'")
|
||||
assert "Hello, World!" in result
|
||||
|
||||
|
||||
def test_read_write_file(toolkit, tmpdir):
|
||||
test_file = tmpdir.join("test_file.txt")
|
||||
content = "Test content"
|
||||
|
||||
toolkit.write_file(str(test_file), content)
|
||||
assert test_file.read() == content
|
||||
|
||||
result = toolkit.read_file(str(test_file))
|
||||
assert "The file content at" in result
|
||||
assert system.is_active(str(test_file))
|
||||
|
||||
|
||||
def test_patch_file(toolkit, tmpdir):
|
||||
test_file = tmpdir.join("test_file.txt")
|
||||
test_file.write("Hello, World!")
|
||||
|
||||
toolkit.read_file(str(test_file)) # Remember the file
|
||||
result = toolkit.patch_file(str(test_file), "World", "Universe")
|
||||
assert "Succesfully replaced before with after" in result
|
||||
assert test_file.read() == "Hello, Universe!"
|
||||
|
||||
|
||||
def test_change_dir(toolkit, tmpdir):
|
||||
subdir = tmpdir.mkdir("subdir")
|
||||
result = toolkit.change_dir(str(subdir))
|
||||
assert result == str(subdir)
|
||||
assert system.cwd == str(subdir)
|
||||
|
||||
|
||||
def test_start_process(toolkit, tmpdir):
|
||||
process_id = toolkit.start_process("python -m http.server 8000")
|
||||
assert process_id > 0
|
||||
|
||||
# Check if the process is in the list of running processes
|
||||
processes = toolkit.list_processes()
|
||||
assert process_id in processes
|
||||
assert "python -m http.server 8000" in processes[process_id]
|
||||
|
||||
|
||||
def test_list_processes(toolkit, tmpdir):
|
||||
process_id1 = toolkit.start_process("python -m http.server 8001")
|
||||
process_id2 = toolkit.start_process("python -m http.server 8002")
|
||||
|
||||
processes = toolkit.list_processes()
|
||||
assert process_id1 in processes
|
||||
assert process_id2 in processes
|
||||
assert "python -m http.server 8001" in processes[process_id1]
|
||||
assert "python -m http.server 8002" in processes[process_id2]
|
||||
|
||||
|
||||
def test_cancel_process(toolkit, tmpdir):
|
||||
process_id = toolkit.start_process("python -m http.server 8003")
|
||||
|
||||
result = toolkit.cancel_process(process_id)
|
||||
assert result == f"process {process_id} cancelled"
|
||||
|
||||
# Verify that the process is no longer in the list
|
||||
processes = toolkit.list_processes()
|
||||
assert process_id not in processes
|
||||
@@ -1,5 +1,5 @@
|
||||
import pytest
|
||||
from goose.utils.check_shell_command import is_dangerous_command
|
||||
from goose.utils.shell import is_dangerous_command
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -3,15 +3,12 @@ from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from exchange import Message
|
||||
from goose.utils.session_file import (
|
||||
is_empty_session,
|
||||
list_sorted_session_files,
|
||||
read_from_file,
|
||||
read_or_create_file,
|
||||
save_latest_session,
|
||||
session_file_exists,
|
||||
write_to_file,
|
||||
)
|
||||
|
||||
|
||||
@@ -20,17 +17,6 @@ def file_path(tmp_path):
|
||||
return tmp_path / "test_file.jsonl"
|
||||
|
||||
|
||||
def test_read_write_to_file(file_path):
|
||||
messages = [
|
||||
Message.user("prompt1"),
|
||||
Message.user("prompt2"),
|
||||
]
|
||||
write_to_file(file_path, messages)
|
||||
assert file_path.exists()
|
||||
|
||||
assert read_from_file(file_path) == messages
|
||||
|
||||
|
||||
def test_read_from_file_non_existing_file(tmp_path):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
read_from_file(tmp_path / "no_existing.json")
|
||||
@@ -49,16 +35,6 @@ def test_read_or_create_file_when_file_not_exist(tmp_path):
|
||||
assert os.path.exists(file_path)
|
||||
|
||||
|
||||
def test_read_or_create_file_when_file_exists(file_path):
|
||||
messages = [
|
||||
Message.user("prompt1"),
|
||||
]
|
||||
write_to_file(file_path, messages)
|
||||
|
||||
assert file_path.exists()
|
||||
assert read_from_file(file_path) == messages
|
||||
|
||||
|
||||
def test_list_sorted_session_files(tmp_path):
|
||||
session_files_directory = tmp_path / "session_files_dir"
|
||||
session_files_directory.mkdir()
|
||||
@@ -98,21 +74,6 @@ def test_session_file_exists_return_true_when_session_file_exists(tmp_path):
|
||||
assert session_file_exists(session_files_directory)
|
||||
|
||||
|
||||
def test_save_latest_session(file_path, tmp_path):
|
||||
messages = [
|
||||
Message.user("prompt1"),
|
||||
Message.user("prompt2"),
|
||||
]
|
||||
write_to_file(file_path, messages)
|
||||
|
||||
messages.append(Message.user("prompt3"))
|
||||
save_latest_session(file_path, messages)
|
||||
|
||||
messages_in_file = read_from_file(file_path)
|
||||
assert messages_in_file == messages
|
||||
assert len(messages_in_file) == 3
|
||||
|
||||
|
||||
def create_session_file(file_path, file_name) -> Path:
|
||||
file = file_path / f"{file_name}.jsonl"
|
||||
file.touch()
|
||||
|
||||
Reference in New Issue
Block a user