mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-08-28 05:25:04 +00:00
refactor(evolve): replace ReActAgent with FlexReActAgent to allow structured output (#265)
- Create FlexReActAgent subclass that overrides _reasoning method to handle tool_choice parameter - Modify auto_memory_planner to use FlexReActAgent instead of ReActAgent - Update base_step.py to accept additional kwargs in add_as_tool method - Change run_job function to merge kwargs properly when calling jobs - Remove redundant imports and constants from file_io.py - Simplify _render_notes_block and rename _replace_or_append_notes to _rebuild_body - Update method calls to use new function names in file_io operations
This commit is contained in:
parent
3cb2579ff7
commit
ef22bfb071
4 changed files with 32 additions and 49 deletions
|
|
@ -188,14 +188,14 @@ class BaseStep(ABC):
|
|||
raise RuntimeError(f"Job {name} not found")
|
||||
return await job(**kwargs)
|
||||
|
||||
def add_as_tool(self, toolkit: Toolkit, job_name: str) -> None:
|
||||
def add_as_tool(self, toolkit: Toolkit, job_name: str, **kwargs) -> None:
|
||||
"""Add the step as a tool to the toolkit."""
|
||||
job: "BaseJob | None" = self.get_job(job_name)
|
||||
if job is None:
|
||||
raise RuntimeError(f"Job {job_name} not found")
|
||||
|
||||
async def run_job(**kwargs) -> ToolResponse:
|
||||
response = await job(**kwargs)
|
||||
async def run_job(**_kwargs) -> ToolResponse:
|
||||
response = await job(**{**_kwargs, **kwargs})
|
||||
return ToolResponse(content=[TextBlock(type="text", text=response.answer)])
|
||||
|
||||
toolkit.register_tool_function(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
import datetime
|
||||
import zoneinfo
|
||||
from typing import Literal
|
||||
|
||||
from agentscope.agent import ReActAgent
|
||||
from agentscope.message import Msg
|
||||
|
||||
|
||||
|
|
@ -27,3 +29,15 @@ def format_history(messages: list[Msg], include_timestamp: bool = True) -> str:
|
|||
header = f"[{speaker} @ {msg.timestamp}]" if include_timestamp else f"[{speaker}]"
|
||||
lines.append(f"{header}\n{text}")
|
||||
return "\n\n".join(lines) or "(empty)"
|
||||
|
||||
|
||||
class FlexReActAgent(ReActAgent):
|
||||
"""ReActAgent subclass that allows structured output without forcing tool_choice='required'."""
|
||||
|
||||
async def _reasoning(
|
||||
self,
|
||||
tool_choice: Literal["auto", "none", "required"] | None = None,
|
||||
) -> Msg:
|
||||
if tool_choice == "required":
|
||||
tool_choice = None
|
||||
return await super()._reasoning(tool_choice)
|
||||
|
|
|
|||
|
|
@ -20,12 +20,11 @@ Output (written to context.response):
|
|||
metadata['memory_updates']: list of ``{path, description}`` dicts.
|
||||
"""
|
||||
|
||||
from agentscope.agent import ReActAgent
|
||||
from agentscope.message import Msg
|
||||
from agentscope.tool import Toolkit
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ._evolve import format_history, now
|
||||
from ._evolve import FlexReActAgent, format_history, now
|
||||
from ..base_step import BaseStep
|
||||
from ...components import R
|
||||
|
||||
|
|
@ -76,7 +75,7 @@ class AutoMemoryPlannerStep(BaseStep):
|
|||
for job_name in self.planner_tools:
|
||||
self.add_as_tool(toolkit, job_name)
|
||||
|
||||
agent = ReActAgent(
|
||||
agent = FlexReActAgent(
|
||||
name="auto_memory_planner",
|
||||
model=self.as_llm,
|
||||
sys_prompt=self.prompt_format("system_prompt"),
|
||||
|
|
|
|||
|
|
@ -414,58 +414,28 @@ def validate_slug(slug: str) -> str | None:
|
|||
_NOTES_OPEN = "<!-- notes:auto -->"
|
||||
_NOTES_CLOSE = "<!-- /notes:auto -->"
|
||||
|
||||
_NOTES_BLOCK_RE = re.compile(
|
||||
rf"{re.escape(_NOTES_OPEN)}(?P<inner>.*?){re.escape(_NOTES_CLOSE)}",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def _wrap_notes_block(inner: str) -> str:
|
||||
return f"{_NOTES_OPEN}\n{inner}\n{_NOTES_CLOSE}"
|
||||
|
||||
|
||||
# Content rendering
|
||||
# -----------------
|
||||
|
||||
|
||||
def _render_notes_block(notes: list[dict]) -> str:
|
||||
"""Render each note as a single line with its full frontmatter inlined.
|
||||
|
||||
Format: ``- [[path]] key1: value1 key2: value2 ...``. ``name`` and
|
||||
``description`` lead (when present) so columns line up across notes;
|
||||
remaining keys follow in frontmatter insertion order. Empty / None
|
||||
values are skipped; newlines in values collapse to spaces so the
|
||||
single-line invariant holds.
|
||||
"""
|
||||
"""Render each note as ``- [[path]] key: val ...`` (one line per note)."""
|
||||
if not notes:
|
||||
return "(none)"
|
||||
lines: list[str] = []
|
||||
for note in notes:
|
||||
meta: dict = note["metadata"]
|
||||
ordered_keys = [k for k in ("name", "description") if k in meta]
|
||||
ordered_keys += [k for k in meta if k not in ("name", "description")]
|
||||
parts = [f"- [[{note['path']}]]"]
|
||||
for key in ordered_keys:
|
||||
value = meta[key]
|
||||
if value is None or value == "":
|
||||
continue
|
||||
value_str = str(value).replace("\r\n", " ").replace("\r", " ").replace("\n", " ")
|
||||
parts.append(f"{key}: {value_str}")
|
||||
keys = [k for k in ("name", "description") if k in meta] + [k for k in meta if k not in ("name", "description")]
|
||||
parts = [f"- [[{note['path']}]]"] + [
|
||||
f"{k}: {str(v).replace(chr(10), ' ')}" for k in keys if (v := meta[k]) not in (None, "")
|
||||
]
|
||||
lines.append(" ".join(parts))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# Body manipulation
|
||||
# -----------------
|
||||
|
||||
|
||||
def _replace_or_append_notes(body: str, fresh_block: str) -> str:
|
||||
"""Replace an existing notes auto block in-place; append at end if absent."""
|
||||
if _NOTES_BLOCK_RE.search(body):
|
||||
replacement = _wrap_notes_block(fresh_block)
|
||||
return _NOTES_BLOCK_RE.sub(lambda m: replacement, body, count=1)
|
||||
suffix = _wrap_notes_block(fresh_block)
|
||||
return f"{body.rstrip()}\n\n{suffix}\n" if body.strip() else f"{suffix}\n"
|
||||
def _rebuild_body(body: str, notes_content: str) -> str:
|
||||
"""Replace or append the auto block, preserving surrounding content."""
|
||||
block = f"{_NOTES_OPEN}\n{notes_content}\n{_NOTES_CLOSE}"
|
||||
if _NOTES_OPEN in body and _NOTES_CLOSE in body:
|
||||
return body.split(_NOTES_OPEN, 1)[0] + block + body.split(_NOTES_CLOSE, 1)[1]
|
||||
return f"{body.rstrip()}\n\n{block}\n" if body.strip() else f"{block}\n"
|
||||
|
||||
|
||||
# Public scan + rebuild
|
||||
|
|
@ -539,7 +509,7 @@ async def refresh_day_index(file_store, date: str, daily_dir: str) -> dict:
|
|||
|
||||
if index_abs.is_file():
|
||||
post = frontmatter.loads(index_abs.read_text(encoding="utf-8"))
|
||||
new_body = _replace_or_append_notes(post.content, notes_block)
|
||||
new_body = _rebuild_body(post.content, notes_block)
|
||||
merged = dict(post.metadata or {})
|
||||
for key, value in fm.items():
|
||||
if not merged.get(key):
|
||||
|
|
@ -548,7 +518,7 @@ async def refresh_day_index(file_store, date: str, daily_dir: str) -> dict:
|
|||
was_created = False
|
||||
else:
|
||||
index_abs.parent.mkdir(parents=True, exist_ok=True)
|
||||
new_body = _wrap_notes_block(notes_block) + "\n"
|
||||
new_body = f"{_NOTES_OPEN}\n{notes_block}\n{_NOTES_CLOSE}\n"
|
||||
was_created = True
|
||||
out = frontmatter.Post(new_body, **fm)
|
||||
index_abs.write_text(frontmatter.dumps(out), encoding="utf-8")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue