feat: lazy load default agent and enhance CLI agent creation with optional checkpointer

This commit is contained in:
X-iZhang
2026-02-11 23:34:38 +00:00
parent f29b254d9a
commit 10266c660f
2 changed files with 52 additions and 54 deletions
+28 -8
View File
@@ -189,21 +189,41 @@ base_middleware = [
]
# Default agent (no checkpointer) — used by langgraph dev / LangSmith / notebooks.
# Built WITHOUT MCP at import time to avoid spawning subprocesses on every import.
# MCP tools are loaded on-demand in create_cli_agent().
_AGENT_KWARGS = _build_base_kwargs(backend, base_middleware)
EvoScientist_agent = create_deep_agent(**_AGENT_KWARGS).with_config({"recursion_limit": 500})
# Lazily constructed on first access so MCP tools are included without
# spawning subprocesses at import time.
_EvoScientist_agent = None
def create_cli_agent(workspace_dir: str | None = None):
"""Create agent with InMemorySaver checkpointer for CLI multi-turn support.
def _get_default_agent():
"""Build the default agent (with MCP, no checkpointer) on first access."""
global _EvoScientist_agent
if _EvoScientist_agent is None:
kwargs = load_mcp_and_build_kwargs(backend, base_middleware)
_EvoScientist_agent = create_deep_agent(**kwargs).with_config(
{"recursion_limit": 500}
)
return _EvoScientist_agent
def __getattr__(name: str):
if name == "EvoScientist_agent":
return _get_default_agent()
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def create_cli_agent(workspace_dir: str | None = None, checkpointer=None):
"""Create agent with checkpointer for CLI multi-turn support.
Args:
workspace_dir: Optional per-session workspace directory. If provided,
creates a fresh backend rooted at this path. If None, uses the
module-level default backend (./workspace).
checkpointer: Optional LangGraph checkpointer. If None, falls back
to ``InMemorySaver`` (non-persistent).
"""
from langgraph.checkpoint.memory import InMemorySaver # type: ignore[import-untyped]
if checkpointer is None:
from langgraph.checkpoint.memory import InMemorySaver # type: ignore[import-untyped]
checkpointer = InMemorySaver()
if workspace_dir:
set_active_workspace(workspace_dir)
@@ -241,5 +261,5 @@ def create_cli_agent(workspace_dir: str | None = None):
return create_deep_agent(
**kwargs,
checkpointer=InMemorySaver(),
checkpointer=checkpointer,
).with_config({"recursion_limit": 500})
+24 -46
View File
@@ -41,7 +41,7 @@ from langchain.agents.middleware.types import (
PrivateStateAttr,
)
from langchain.tools import ToolRuntime
from langchain_core.messages import AnyMessage, HumanMessage, AIMessage
from langchain_core.messages import AnyMessage, HumanMessage, filter_messages
from langchain_core.runnables.config import RunnableConfig
from langgraph.runtime import Runtime
@@ -540,69 +540,47 @@ class EvoMemoryMiddleware(AgentMiddleware):
# -- extraction ----------------------------------------------------------
def _extract(self, model: BaseChatModel, memory: str, messages: list[AnyMessage]) -> dict[str, Any]:
"""Run LLM extraction on recent messages."""
import json
# Build conversation string from recent messages (last 30)
recent = messages[-30:]
@staticmethod
def _build_extraction_prompt(memory: str, messages: list[AnyMessage]) -> str:
"""Build the extraction prompt from recent human/AI messages."""
recent = filter_messages(messages[-30:], include_types=["human", "ai"])
conv_parts = []
for msg in recent:
if isinstance(msg, HumanMessage):
role = "user"
elif isinstance(msg, AIMessage):
role = "assistant"
else:
continue
role = "user" if isinstance(msg, HumanMessage) else "assistant"
content = msg.content if isinstance(msg.content, str) else str(msg.content)
conv_parts.append(f"[{role}]: {content}")
conversation = "\n".join(conv_parts)
prompt = EXTRACTION_PROMPT.format(
return EXTRACTION_PROMPT.format(
current_memory=memory,
conversation=conversation,
conversation="\n".join(conv_parts),
)
@staticmethod
def _parse_extraction_response(text: str) -> dict[str, Any]:
"""Parse JSON from an LLM extraction response."""
import json
json_match = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
if json_match:
text = json_match.group(1)
return json.loads(text.strip())
def _extract(self, model: BaseChatModel, memory: str, messages: list[AnyMessage]) -> dict[str, Any]:
"""Run LLM extraction on recent messages."""
prompt = self._build_extraction_prompt(memory, messages)
try:
response = model.invoke(prompt)
text = response.content if isinstance(response.content, str) else str(response.content)
# Extract JSON from response (may be wrapped in ```json ... ```)
json_match = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
if json_match:
text = json_match.group(1)
return json.loads(text.strip())
return self._parse_extraction_response(text)
except Exception as e: # noqa: BLE001
logger.warning("Memory extraction failed: %s", e)
return {}
async def _aextract(self, model: BaseChatModel, memory: str, messages: list[AnyMessage]) -> dict[str, Any]:
import json
recent = messages[-30:]
conv_parts = []
for msg in recent:
if isinstance(msg, HumanMessage):
role = "user"
elif isinstance(msg, AIMessage):
role = "assistant"
else:
continue
content = msg.content if isinstance(msg.content, str) else str(msg.content)
conv_parts.append(f"[{role}]: {content}")
conversation = "\n".join(conv_parts)
prompt = EXTRACTION_PROMPT.format(
current_memory=memory,
conversation=conversation,
)
prompt = self._build_extraction_prompt(memory, messages)
try:
response = await model.ainvoke(prompt)
text = response.content if isinstance(response.content, str) else str(response.content)
json_match = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
if json_match:
text = json_match.group(1)
return json.loads(text.strip())
return self._parse_extraction_response(text)
except Exception as e: # noqa: BLE001
logger.warning("Memory extraction failed: %s", e)
return {}