mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-11-02 07:13:50 +00:00
- 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
82 lines
3.2 KiB
Python
82 lines
3.2 KiB
Python
"""compressor — compress text via a direct LLM call (no agent)."""
|
|
|
|
from agentscope.message import UserMsg
|
|
from agentscope.model import ChatResponse
|
|
|
|
from ..base_step import BaseStep
|
|
from ...components import R
|
|
|
|
|
|
@R.register("compressor_step")
|
|
class CompressorStep(BaseStep):
|
|
"""Compress ``text`` with a direct LLM call (no agent wrapper).
|
|
|
|
Inputs (from RuntimeContext):
|
|
text (str, required): the text to compress.
|
|
queries (list[str], optional, default []): when provided, compression
|
|
keeps anything potentially relevant to any of the queries and may
|
|
drop content that is certainly irrelevant and certainly unhelpful
|
|
for all of them.
|
|
|
|
Output (written to context.response.answer):
|
|
The compressed text.
|
|
"""
|
|
|
|
async def execute(self):
|
|
assert self.context is not None
|
|
text: str = self.context.get("text", "") or ""
|
|
raw_queries = self.context.get("queries") or []
|
|
queries: list[str] = [str(q).strip() for q in raw_queries if str(q).strip()]
|
|
|
|
if not text.strip():
|
|
self.context.response.success = False
|
|
self.context.response.answer = "Skipped: empty text"
|
|
return
|
|
|
|
if queries:
|
|
queries_block = "\n".join(f"- {q}" for q in queries)
|
|
user_message = self.prompt_format("compress_query_prompt", queries=queries_block, text=text)
|
|
else:
|
|
user_message = self.prompt_format("compress_prompt", text=text)
|
|
|
|
result = await self.as_llm([UserMsg(name="user", content=user_message)])
|
|
compressed = await self._response_text(result)
|
|
self.logger.info(
|
|
f"[{self.name}] compressed {len(text)} -> {len(compressed)} chars (queries={len(queries)})",
|
|
)
|
|
|
|
self.context.response.success = True
|
|
self.context.response.answer = compressed
|
|
self.context.response.metadata.update(
|
|
{
|
|
"queries": queries,
|
|
"original_length": len(text),
|
|
"compressed_length": len(compressed),
|
|
},
|
|
)
|
|
|
|
@staticmethod
|
|
async def _response_text(result) -> str:
|
|
"""Extract text blocks from a streaming or non-streaming ChatResponse.
|
|
|
|
The async-iteration check is done on the type: ``ChatResponse`` inherits
|
|
agentscope's ``DictMixin`` whose instance ``__getattr__`` raises
|
|
``KeyError`` (not ``AttributeError``), so ``hasattr(result, ...)``
|
|
would crash instead of returning False.
|
|
"""
|
|
if hasattr(type(result), "__aiter__"):
|
|
last: ChatResponse | None = None
|
|
async for chunk in result:
|
|
last = chunk
|
|
result = last
|
|
if result is None:
|
|
return ""
|
|
# Content blocks may be plain dicts or pydantic models (TextBlock/ThinkingBlock).
|
|
parts: list[str] = []
|
|
for block in result.content or []:
|
|
if isinstance(block, dict):
|
|
if block.get("type") == "text":
|
|
parts.append(str(block.get("text") or ""))
|
|
elif getattr(block, "type", None) == "text":
|
|
parts.append(str(getattr(block, "text", "") or ""))
|
|
return "".join(parts).strip()
|