Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/autotests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ jobs:
echo "Waiting for agent to become ready..."
ready=
for i in $(seq 1 90); do
if docker logs omega 2>&1 | grep -qE "CHARS_SENT: [0-9]+"; then
if docker logs omega 2>&1 | grep -qE "iteration 1"; then
echo "Agent ready after ${i}s"
ready=1
break
Expand Down
8 changes: 4 additions & 4 deletions Autotests/mock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ Notes:
- `TEST_SERVER_IP=172.17.0.1` is the host's docker-bridge address used by both the mock LLM provider and the test channel client.
- The container is created with the name `omega` (the script default).

Wait until the agent loop is up. The first runtime `CHARS_SENT:` line (with a byte count after the colon) in the container log marks the end of `initChannels` / `initMemory` and the start of real iterations; the bare `CHARS_SENT:` string also appears earlier as part of the MeTTa source dump, so match on the numeric form to avoid a premature exit:
Wait until the agent loop is up. The first runtime `iteration 1` line (with a byte count after the colon) in the container log marks the end of `initChannels` / `initMemory` and the start of real iterations; the bare `iteration` string also appears earlier as part of the MeTTa source dump, so match on the numeric form to avoid a premature exit:

```
until docker logs omega 2>&1 | grep -qE "CHARS_SENT: [0-9]+"; do sleep 2; done
until docker logs omega 2>&1 | grep -qE "iteration 1"; do sleep 2; done
```

## 4. Configure the test environment
Expand Down Expand Up @@ -305,7 +305,7 @@ Four-step pipeline: search NY weather → write `w.txt` with the forecast → wr
Verifies the one-iteration carry of `LAST_SKILL_USE_RESULTS`. Output of a skill call in turn N is exposed to the LLM at turn N+1 via this prompt section. The test does not require the agent to "behave intelligently"; it confirms the carry exists.

- Mock answer (turn 1): `(metta "(+ 1 1)")`.
- Checks: the docker log line `CHARS_SENT:` for the next iteration contains a `LAST_SKILL_USE_RESULTS` section that reflects the metta output.
- Checks: the docker log line `REQUEST:` for the next iteration contains a last tool call section that reflects the metta output.

### 26. test_memory_history_byte_window_truncation_mock.py

Expand All @@ -327,7 +327,7 @@ A `(pin ...)` emitted in turn 1 must land in `history.metta` and remain inside t
Negative test: a `(pin ...)` emitted within an iteration is NOT visible inside that same iteration's HISTORY context. The prompt is assembled before skill evaluation, so the pin block, written by `addToHistory` at the end of the iteration, only enters HISTORY at the next prompt-build.

- Mock answer: `(pin "<marker>")` followed by a `(send ...)`.
- Checks: the `CHARS_SENT` line carrying the PROMPT for the iteration that contained the pin does NOT contain the pin's unique marker; the next iteration's `CHARS_SENT` line does.
- Checks: the `REQUEST` line carrying the PROMPT for the iteration that contained the pin does NOT contain the pin's unique marker; the next iteration's `REQUEST` line does.

### 29. test_transition_episodes_after_eviction_mock.py

Expand Down
91 changes: 49 additions & 42 deletions Autotests/mock/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from rpc import Rpc, IPCClient, IPCServer
from contextlib import contextmanager
import threading
from providers import *

LLM_MOCK_PORT = 9765

Expand All @@ -23,53 +24,47 @@ def __init__(self, address):
def stop(self, timeout=None):
self._rpc.stop(timeout)

def chat(self, content):
user = content.rsplit(":-:-:-:", 1)
if len(user) < 2:
return ""

try:
body = eval(user[1])[1]
except SyntaxError:
return ""

# The agent escapes punctuation that would confuse its s-exp
# parser ('->_apostrophe_, "->_quote_, \n->_newline_) before
# the text reaches chat(). set_answer stores the literal
# prompt key, so try the raw body first, then the normalized
# form so prompts with quotes/apostrophes/newlines still match.
def normalize(text):
return (text
.replace("_apostrophe_", "'")
.replace("_quote_", '"')
.replace("_newline_", "\n"))
def chat(self, request: LLMRequest) -> LLMResponse:
user = [m for m in request.messages if m.role == "user"]
answer = None
if len(user) > 0:
body = user[-1].content

# The agent escapes punctuation that would confuse its s-exp
# parser ('->_apostrophe_, "->_quote_, \n->_newline_) before
# the text reaches chat(). set_answer stores the literal
# prompt key, so try the raw body first, then the normalized
# form so prompts with quotes/apostrophes/newlines still match.
def normalize(text):
return (text
.replace("_apostrophe_", "'")
.replace("_quote_", '"')
.replace("_newline_", "\n"))

with self._lock:
answer = self._answers.get(body) or self._answers.get(normalize(body))
if answer:
print(f"[LlmMockAgent] Mock answers: {answer}")
return answer

# IRC may deliver multiple PRIVMSGs in one agent iteration; the
# agent concatenates them with " | " between speakers. Split
# and look up each fragment individually so a registered answer
# is not missed when several messages arrive together.
fragments = body.split(" | ")
for fragment in fragments:
if ": " not in fragment:
continue
prompt = fragment.split(": ", 1)[1]
with self._lock:
a = self._answers.get(normalize(prompt)) or self._answers.get(prompt)
if a:
answer = a
answer = self._answers.get(body) or self._answers.get(normalize(body))

if not answer:
# IRC may deliver multiple PRIVMSGs in one agent iteration; the
# agent concatenates them with " | " between speakers. Split
# and look up each fragment individually so a registered answer
# is not missed when several messages arrive together.
fragments = body.split(" | ")
for fragment in fragments:
if ": " not in fragment:
continue
prompt = fragment.split(": ", 1)[1]
with self._lock:
a = self._answers.get(normalize(prompt)) or self._answers.get(prompt)
if a:
answer = a

if answer:
print(f"[LlmMockAgent] Mock answers: {answer}")
return answer
return self._make_llm_response(answer)
else:
print(f"[LlmMockAgent] Mock doesn't have answer for: {body}")
return ""
return LLMResponse()

def on_set_answer(self, args):
with self._lock:
Expand All @@ -80,9 +75,21 @@ def on_set_answer(self, args):
return True

def on_ping(self, args):
print(f'[LlmMockAgent] Mock ping request processed')
print('[LlmMockAgent] Mock ping request processed')
return True

def _make_llm_response(self, calls: [(str, dict[str, str])]) -> LLMResponse:
response = LLMResponse()
id = 0
for (func, args) in calls:
response.add_tool_call(LLMToolCall()
.with_name(func)
.with_id(f"mockid#{id}")
.with_arguments(args))
id = id + 1
return response


class LlmMockController:

def __init__(self, address):
Expand All @@ -100,7 +107,7 @@ def set_answer(self, request, response, timeout=10):
return True

def ping(self, timeout=None):
print(f'[LlmMockController] Ping agent')
print('[LlmMockController] Ping agent')
result = self._rpc.request('ping', {})
if result.get(timeout) != True:
print(f'[LlmMockController] Did not get answer on ping in {timeout} seconds')
Expand Down
2 changes: 1 addition & 1 deletion Autotests/mock/test_convert_format_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def test_convert_md_to_txt_mock(llm, comm):
)
llm.set_answer(
prompt,
f'(shell "cp {SOURCE_FILE} {DEST_FILE}")',
[("shell", {"cmd": f"cp {SOURCE_FILE} {DEST_FILE}"})]
)
if not comm.send_message(prompt):
c.fail("comm", "could not deliver prompt within 60s")
Expand Down
7 changes: 4 additions & 3 deletions Autotests/mock/test_create_empty_file_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ def test_create_empty_file_mock(llm, comm):
"(create the directory if needed). The file can be empty.",
)
llm.set_answer(
prompt,
f'(shell "mkdir -p {TARGET_DIR}") '
f'(write-file "{TARGET_FILE}" "")',
prompt, [
("shell", { "cmd": f"mkdir -p {TARGET_DIR}" }),
("write-file", { "filename": f"{TARGET_FILE}", "content": "" })
]
)
if not comm.send_message(prompt):
c.fail("comm", "could not deliver prompt within 60s")
Expand Down
4 changes: 3 additions & 1 deletion Autotests/mock/test_create_file_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ def test_hello_file(llm, comm):
f"Please overwrite {TARGET_FILE} so it contains exactly the single "
"word Hello (no quotes, no extra newlines, create the directory if needed).",
)
llm.set_answer(prompt, f'(shell "mkdir -p /tmp/testcat") (write-file "/tmp/testcat/hello.txt" "Hello")')
llm.set_answer(prompt, [("shell", { "cmd": "mkdir -p /tmp/testcat" }),
("write-file", { "filename": "/tmp/testcat/hello.txt",
"content": "Hello" })])

if not comm.send_message(prompt):
c.fail("comm", "could not deliver prompt within 60s")
Expand Down
9 changes: 5 additions & 4 deletions Autotests/mock/test_create_script_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ def test_create_date_script_mock(llm, comm):
"Create the directory if needed.",
)
llm.set_answer(
prompt,
f'(shell "mkdir -p {TARGET_DIR}") '
f'(write-file "{TARGET_FILE}" "#!/bin/bash\\ndate\\n") '
f'(shell "chmod +x {TARGET_FILE}")',
prompt, [
("shell", { "cmd": f"mkdir -p {TARGET_DIR}" }),
("write-file", { "filename": f"{TARGET_FILE}", "content": "#!/bin/bash\\ndate\\n" }),
("shell", { "cmd": f"chmod +x {TARGET_FILE}" }),
]
)
if not comm.send_message(prompt):
c.fail("comm", "could not deliver prompt within 60s")
Expand Down
2 changes: 1 addition & 1 deletion Autotests/mock/test_credentials_scrubbed_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def test_agent_env_has_no_secrets(llm, comm):
c.run_id,
f"Use the shell skill to write your environment to a file: shell env > {DUMP}",
)
llm.set_answer(prompt, f'(shell "env > {DUMP}")')
llm.set_answer(prompt, [("shell", { "cmd": f"env > {DUMP}" })])

if not comm.send_message(prompt):
c.fail("comm", "could not deliver prompt within 60s")
Expand Down
2 changes: 1 addition & 1 deletion Autotests/mock/test_edit_add_timestamp_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def test_edit_add_timestamp_mock(llm, comm):
# real test.
llm.set_answer(
prompt,
f'(shell "date -Iseconds >> {TARGET_FILE}")',
[("shell", { "cmd": f"date -Iseconds >> {TARGET_FILE}" })]
)
if not comm.send_message(prompt):
c.fail("comm", "could not deliver prompt within 60s")
Expand Down
2 changes: 1 addition & 1 deletion Autotests/mock/test_edit_append_line_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_edit_append_line_mock(llm, comm):
)
llm.set_answer(
prompt,
f'(shell "printf \'%s\\\\n\' {LINE4_EXPECTED} >> {TARGET_FILE}")',
[("shell", { "cmd": f"printf \'%s\\\\n\' {LINE4_EXPECTED} >> {TARGET_FILE}" })]
)
if not comm.send_message(prompt):
c.fail("comm", "could not deliver prompt within 60s")
Expand Down
2 changes: 1 addition & 1 deletion Autotests/mock/test_edit_delete_line_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_edit_delete_line_mock(llm, comm):
)
llm.set_answer(
prompt,
f'(shell "sed -i 2d {TARGET_FILE}")',
[("shell", { "cmd": f"sed -i 2d {TARGET_FILE}" })]
)
if not comm.send_message(prompt):
c.fail("comm", "could not deliver prompt within 60s")
Expand Down
11 changes: 6 additions & 5 deletions Autotests/mock/test_git_local_commit_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,12 @@ def test_git_local_commit_mock(llm, comm):
# needs the message in single-quoted shell string while the s-exp
# arg itself is double-quoted, so we escape inner quotes.
llm.set_answer(
prompt,
f'(shell "git -C {TARGET_DIR} init") '
f'(write-file "{commit_path}" "{marker}") '
f'(shell "git -C {TARGET_DIR} add -A") '
f'(shell "git -C {TARGET_DIR} commit -m \\"add hello {c.run_id}\\"")',
prompt, [
("shell", { "cmd": f"git -C {TARGET_DIR} init" }),
("write-file", { "filename": f"{commit_path}", "content": f"{marker}" }),
("shell", { "cmd": f"git -C {TARGET_DIR} add -A" }),
("shell", { "cmd": f'git -C {TARGET_DIR} commit -m "add hello {c.run_id}"' })
]
)
if not comm.send_message(prompt):
c.fail("comm", "could not deliver prompt within 60s")
Expand Down
2 changes: 1 addition & 1 deletion Autotests/mock/test_git_pull_public_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def test_git_pull_public_mock(llm, comm):
)
llm.set_answer(
prompt,
f'(shell "{clone_command}")',
[("shell", { "cmd": f"{clone_command}" })]
)
if not comm.send_message(prompt):
c.fail("comm", "could not deliver prompt within 60s")
Expand Down
16 changes: 8 additions & 8 deletions Autotests/mock/test_io_policy_skill_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ def test_get_io_policy_mock(llm, comm):
prompt = make_prompt(c.run_id, "Check your IO policy.")
llm.set_answer(
request=prompt,
response=(
f'(send "Checking my io policy {c.run_id}")\n'
'(get-io-policy)'
)
response=([
("send", { "content": f"Checking my io policy {c.run_id}" }),
("get-io-policy", {})
])
)
if not comm.send_message(prompt):
c.fail("comm", "could not deliver prompt within timeout")
Expand All @@ -51,10 +51,10 @@ def test_get_io_policy_mock(llm, comm):
prompt = make_prompt(c.run_id, "Retrieve the current filesystem access policy.")
llm.set_answer(
request=prompt,
response=(
f'(metta (write-file "{JSON_POLICY_OUTPUT_PATH}" (get-io-policy)))\n'
f'(send "Policy checked for {c.run_id}")'
)
response=([
("metta", { "sexpression": f'(write-file "{JSON_POLICY_OUTPUT_PATH}" (get-io-policy))' }),
("send", { "content": f"Policy checked for {c.run_id}" })
])
)
if not comm.send_message(prompt):
c.fail("comm", "could not deliver prompt within timeout")
Expand Down
23 changes: 13 additions & 10 deletions Autotests/mock/test_last_skill_results_visible_next_turn_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
of (metta ...), (query ...), (shell ...) etc. without persisting it.

Turn 1 mock answer dictates a metta computation. We then read the
docker log to find the CHARS_SENT line for the NEXT iteration and
confirm it contains the LAST_SKILL_USE_RESULTS marker.
docker log to find the REQUEST line for the NEXT iteration and
confirm it contains the last tool call results marker.

Run:
pytest test_last_skill_results_visible_next_turn_mock.py -s
Expand Down Expand Up @@ -46,7 +46,10 @@ def test_last_skill_results_visible_next_turn_mock(llm, comm):
# the next iteration's LAST_SKILL_USE_RESULTS.
llm.set_answer(
prompt1,
f'(metta "(quote {sentinel})") (send "computed")',
[
("metta", { "sexpression": f"(quote {sentinel})" }),
("send", { "content": "computed" })
]
)
if not comm.send_message(prompt1):
c.fail("comm-1", "could not deliver turn 1 prompt within 60s")
Expand All @@ -69,22 +72,22 @@ def test_last_skill_results_visible_next_turn_mock(llm, comm):
c.step("wait for the agent to start a fresh iteration")
time.sleep(20)

c.step("verify next iteration's CHARS_SENT contains LAST_SKILL_USE_RESULTS with sentinel")
c.step("verify next iteration's REQUEST contains tool call results with sentinel")
logs = docker_logs()
# We look for any CHARS_SENT line after our metta call that carries
# the sentinel inside the LAST_SKILL_USE_RESULTS section.
# We look for any REQUEST line after our metta call that carries
# the sentinel inside the tool call results section.
chars_sent_lines = [
ln for ln in logs.split("\n")
if "CHARS_SENT:" in ln and "LAST_SKILL_USE_RESULTS:" in ln
if "REQUEST:" in ln and "[TOOL CALL]" in ln
]
relevant = [
ln for ln in chars_sent_lines
if sentinel in ln
]
if not relevant:
c.fail("sentinel in lastresults",
f"no CHARS_SENT line carries {sentinel!r} in "
f"LAST_SKILL_USE_RESULTS. Total CHARS_SENT lines "
f"no REQUEST line carries {sentinel!r} in "
f"tool call results. Total REQUEST lines "
f"checked: {len(chars_sent_lines)}")
c.ok("sentinel in lastresults",
f"found in {len(relevant)} subsequent iteration prompt(s)")
Expand All @@ -94,6 +97,6 @@ def test_last_skill_results_visible_next_turn_mock(llm, comm):
c.fail("no unconditional failure instruction",
f"found deprecated prompt text: {deprecated_instruction!r}")
c.ok("no unconditional failure instruction",
"LAST_SKILL_USE_RESULTS contains feedback without failure guidance")
"tool call results contains feedback without failure guidance")

c.done()
Loading
Loading