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:
jinliyl 2026-05-29 15:19:56 +08:00 committed by GitHub
parent 3cb2579ff7
commit ef22bfb071
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 32 additions and 49 deletions

View file

@ -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(

View file

@ -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)

View file

@ -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"),

View file

@ -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")