feat: lazy load default agent and enhance CLI agent creation with optional checkpointer
This commit is contained in:
@@ -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
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user