mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-08-28 05:25:04 +00:00
* chore(benchmark): isolate dataset/workspaces/results per benchmark
- Move shared benchmark/{datasets,memory_workspaces,results} into per-benchmark subdirs benchmark/<name>/{dataset,workspaces,results}
- Update beam/longmemeval config.yaml and run.py path defaults
- Relocate longmemeval download.py to benchmark/longmemeval/ (downloads into dataset/ subdir); inline dataset download docs into README
- Update .gitignore: benchmark/*/{dataset,workspaces,results}/
- Move result-{beam,longmemeval}.md to benchmark/results_md/ and drop result- prefix; update README links
- Fix stale path refs in llm_judge.py and logs/demo_search_format.py
* feat(benchmark): add read tool to agentic answer and update BEAM results
- Add 'read' to job_tools in BaseAgenticAnswerStep for file reading capability
- Document read tool usage in lme/agentic_answer.yaml system prompt
- Update result-beam.md with latest evaluation scores (OVERALL: 0.623/0.580)
* feat(auto_memory): add source line-number markers for note traceability
- Add _format_history hook in AutoMemoryStep with line-number annotation
- Override in BeamAutoMemoryStep to prefix each turn with [Ln] for citation
- Add session_file variable to prompt templates for source marker paths
- Simplify repeated extraction rules by referencing system prompt
- Enhance agentic_answer search strategy (multi-search, read tool hint)
- Add warning log on ReadStep failure
* feat(beam): enhance auto_memory with source markers and pilot ingest tooling
* refactor(beam): rename max_chunk_words to max_segment_words, drop one-off pilot scripts
* feat: add CompressorStep and search_v2 dual-mode session compression
- Add CompressorStep (reme/steps/evolve/compressor.py) for direct LLM
text compression with optional query-guided relevance filtering
- Extend search_v2_step to support query-aware and query-independent
session transcript compression via _compress injected kwargs
- Refactor _source_format.py: split into render_chunk_entries +
join_chunk_entries; session chunks now render line-aligned with
L<n>: prefixes for verbatim/compressed parity
- Add JOB_TOOLS and INJECTED_JOB_KWARGS to BaseAgenticAnswerStep for
per-subclass tool and parameter injection
- LmeAgenticAnswerStep injects _search._compress payload to enable
query-aware compression during benchmark evaluation
- Record compression ablation results in result-longmemeval.md
- Add unit tests for CompressorStep and search compression paths
* refactor(compress): relax session compression to lenient format-preserving strategy and update LME results
* refactor(benchmark): make session compression config-driven via compress_session flag
Move session-transcript compression from LME hard-coded injection to a
runtime context flag set by evaluation.compress_session in each
benchmark config. Compression is off by default for both BEAM and LME,
and BaseAgenticAnswerStep now conditionally injects the _search compress
payload only when the flag is truthy.
* feat(lme/auto_memory): add source attribution markers with line numbers
Add _format_history to annotate each turn with [Ln] line numbers and
expose {session_file} in prompts so the agent can emit bare wikilink-style
source markers like [[session/dialog/s1.jsonl#L1-L2,L5-L6]] at the end
of factual entries. Consolidate the per-prompt body/format rules into
references to the system prompt to avoid drift, and add frontmatter-
protection guidance for the edit tool.
* feat: improve agentic answer prompt and update beam 100K results
- Strengthen abstention rule: prohibit extrapolation from related but
non-direct evidence
- Add multi-angle search after preliminary answer to check for
conflicting/supplementary/updated information
- Add max-iteration fallback to 'Information not found'
- Update beam.md with 100K results (agentscope 2.0.4.post1, from scratch)
including per-type token consumption and memory construction stats
- config.yaml: 100K dataset, 20 workers for BEAM evaluation
- run.py: add memory construction token usage tracking (default agent)
- Overall: 0.635 → 0.654 (+0.019), contradiction_resolution: 0.338 → 0.478
(+0.140), abstention: 0.500 → 0.525 (+0.025)
* feat(read): add session-aware formatting for read tool and update BEAM eval
- Add truncate_session_output in _file_io.py to render jsonl session
lines as [speaker @ time] content before byte-budget truncation
- Add read_step_format_session flag to ReadStep, honoring injected
job kwargs (precedence) and YAML fallback
- Inject read_step_format_session=True into BaseAgenticAnswerStep
so agentic answer reads render session transcripts human-readably
- Refine BEAM agentic_answer prompt: continue multi-angle search
after preliminary answer, forbid fabrication/extrapolation
- Update BEAM config to 1M variant and add sequential 100K-eval /
1M-build shell script
- Refresh benchmark/results_md/beam.md with latest results
* chore(config): disable expand_links in beam and lme search_v2 configs
* refactor(beam): drop one-off sequential 100K-eval-then-1M-build script
* fix(benchmark): add compressor job to beam config and fix BEAM clone instructions
- Add compressor job and compressor as_llm component to reme/config/beam.yaml
(aligned with lme.yaml) so that compress_session: true works for BEAM
- Add graceful degradation guard in search_v2._compress_session_entries:
when the compressor job is missing from the active config, log a warning
and skip compression instead of raising 'Job compressor not found'.
Skipped when there is no app_context so unit tests mocking run_job still
drive compression behavior.
- Fix BEAM download instructions in README.md/README_ZH.md: add mkdir -p
before cd benchmark/beam/dataset (the directory is gitignored and absent
in a fresh clone)
* fix(steps): guard compressor exceptions and fix ReadStep boolean override
1. search_v2: catch per-entry exceptions from run_job('compressor') inside
compress() so asyncio.gather never propagates a compressor failure (e.g.
temporary LLM outage). The failing entry keeps its original body while
remaining entries are still compressed, preserving already-retrieved
search results.
2. read: replace 'context_value or yaml_value' with an existence check so
that a runtime-injected False can explicitly disable a YAML-true
read_step_format_session flag.
Add focused unit tests for both paths.
* fix(search_v2): use existence check for strict_date_filter boolean override
Replace 'context_value or yaml_value' with an existence-based check so
that a runtime-injected False can explicitly disable a YAML-true
strict_date_filter flag, consistent with the read_step_format_session fix.
* refactor(search): simplify strict_date_filter fallback to truthiness-or
* style(test): rename unused param to satisfy pylint W0613
---------
Co-authored-by: sa-buc <jiangniurou.xyf@dail-algo011164204033.ET135>
128 lines
4.6 KiB
Python
128 lines
4.6 KiB
Python
"""Unit tests for BEAM auto-memory segmented ingestion and line numbering."""
|
|
|
|
# pylint: disable=missing-class-docstring,missing-function-docstring,protected-access
|
|
|
|
from agentscope.message import Msg
|
|
|
|
from reme.components.runtime_context import RuntimeContext
|
|
from reme.steps.benchmark.beam.auto_memory import (
|
|
BeamAutoMemoryStep,
|
|
split_turn_segments,
|
|
)
|
|
|
|
|
|
def _msg(role: str, words: int, created_at: str = "2024-03-01T09:00:00") -> Msg:
|
|
return Msg(
|
|
name=role,
|
|
role=role,
|
|
content=[{"type": "text", "text": " ".join(["w"] * words)}],
|
|
created_at=created_at,
|
|
)
|
|
|
|
|
|
def _dialog(n_turns: int, words_per_msg: int) -> list[Msg]:
|
|
messages: list[Msg] = []
|
|
for _ in range(n_turns):
|
|
messages.append(_msg("user", words_per_msg))
|
|
messages.append(_msg("assistant", words_per_msg))
|
|
return messages
|
|
|
|
|
|
class TestSplitTurnSegments:
|
|
def test_empty(self):
|
|
assert not split_turn_segments([], 100)
|
|
|
|
def test_disabled_returns_single_segment(self):
|
|
messages = _dialog(3, 10)
|
|
segments = split_turn_segments(messages, 0)
|
|
assert len(segments) == 1
|
|
assert segments[0] == (0, messages)
|
|
|
|
def test_under_limit_single_segment(self):
|
|
messages = _dialog(3, 10) # 60 words total
|
|
segments = split_turn_segments(messages, 100)
|
|
assert len(segments) == 1
|
|
assert segments[0][0] == 0
|
|
assert segments[0][1] == messages
|
|
|
|
def test_splits_at_turn_boundaries(self):
|
|
# 4 turns x 20 words each; limit 40 -> 2 turns per segment
|
|
messages = _dialog(4, 10)
|
|
segments = split_turn_segments(messages, 40)
|
|
assert len(segments) == 2
|
|
offsets = [offset for offset, _ in segments]
|
|
assert offsets == [0, 4]
|
|
# Every segment starts with a user message and ends with an assistant
|
|
for _, segment in segments:
|
|
assert segment[0].role == "user"
|
|
assert segment[-1].role == "assistant"
|
|
# No message lost or duplicated, order preserved
|
|
flattened = [m for _, segment in segments for m in segment]
|
|
assert flattened == messages
|
|
|
|
def test_never_splits_inside_a_turn(self):
|
|
# One turn alone exceeds the limit -> becomes its own oversized segment
|
|
messages = [
|
|
_msg("user", 5),
|
|
_msg("assistant", 5),
|
|
_msg("user", 50),
|
|
_msg("assistant", 50), # 100-word turn > limit 60
|
|
_msg("user", 5),
|
|
_msg("assistant", 5),
|
|
]
|
|
segments = split_turn_segments(messages, 60)
|
|
assert [offset for offset, _ in segments] == [0, 2, 4]
|
|
assert [len(segment) for _, segment in segments] == [2, 2, 2]
|
|
|
|
def test_offsets_are_original_indices(self):
|
|
messages = _dialog(5, 30) # 60 words per turn
|
|
segments = split_turn_segments(messages, 120)
|
|
# 2 turns per segment -> offsets 0, 4, 8
|
|
assert [offset for offset, _ in segments] == [0, 4, 8]
|
|
for offset, segment in segments:
|
|
for i, msg in enumerate(segment):
|
|
assert msg is messages[offset + i]
|
|
|
|
def test_multi_assistant_turn_stays_together(self):
|
|
messages = [
|
|
_msg("user", 10),
|
|
_msg("assistant", 10),
|
|
_msg("assistant", 10),
|
|
_msg("user", 10),
|
|
_msg("assistant", 10),
|
|
]
|
|
segments = split_turn_segments(messages, 30)
|
|
assert [offset for offset, _ in segments] == [0, 3]
|
|
assert len(segments[0][1]) == 3
|
|
|
|
|
|
class TestFormatHistoryLineNumbers:
|
|
def _step(self, offset: int) -> BeamAutoMemoryStep:
|
|
step = BeamAutoMemoryStep(name="beam_auto_memory_step", backend="beam_auto_memory_step")
|
|
step.context = RuntimeContext(
|
|
session_id="beam_1M_1_batch1",
|
|
beam_line_offset=offset,
|
|
)
|
|
return step
|
|
|
|
def test_numbers_start_at_one_without_offset(self):
|
|
step = self._step(0)
|
|
history = step._format_history(_dialog(2, 3))
|
|
assert "[L1 | user @" in history
|
|
assert "[L4 | assistant @" in history
|
|
assert "lines 1-4" in history
|
|
assert "session/dialog/beam_1M_1_batch1.jsonl" in history
|
|
|
|
def test_numbers_use_original_file_offset(self):
|
|
step = self._step(40)
|
|
history = step._format_history(_dialog(2, 3))
|
|
assert "[L41 | user @" in history
|
|
assert "[L44 | assistant @" in history
|
|
assert "lines 41-44" in history
|
|
assert "[L1 " not in history
|
|
|
|
def test_build_messages_passes_msg_objects_through(self):
|
|
step = self._step(0)
|
|
messages = _dialog(2, 3)
|
|
rebuilt = step._build_messages(messages)
|
|
assert [m.id for m in rebuilt] == [m.id for m in messages]
|