Add runtime context middleware (#255)
This commit is contained in:
@@ -20,7 +20,6 @@ Usage:
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from langchain.agents.middleware import AgentMiddleware, HumanInTheLoopMiddleware
|
||||
@@ -28,7 +27,7 @@ from langchain.agents.middleware import AgentMiddleware, HumanInTheLoopMiddlewar
|
||||
from . import paths as _paths_mod
|
||||
from .config import apply_config_to_env, get_effective_config
|
||||
from .paths import set_active_workspace, set_workspace_root
|
||||
from .prompts import RESEARCHER_INSTRUCTIONS, get_system_prompt
|
||||
from .prompts import get_system_prompt
|
||||
|
||||
# Suppress noisy warnings from deepagents skill loader (non-string frontmatter fields, etc.)
|
||||
logging.getLogger("deepagents.middleware.skills").setLevel(logging.ERROR)
|
||||
@@ -198,6 +197,7 @@ def _inject_subagent_middleware(subs: list[dict]) -> None:
|
||||
ContextOverflowMapperMiddleware,
|
||||
ToolErrorHandlerMiddleware,
|
||||
create_context_editing_middleware,
|
||||
create_runtime_context_middleware,
|
||||
)
|
||||
|
||||
for sa in subs:
|
||||
@@ -206,21 +206,13 @@ def _inject_subagent_middleware(subs: list[dict]) -> None:
|
||||
# No ``model=`` — subagents share the main agent's model,
|
||||
# so defer to the factory's ``_ensure_chat_model()`` fallback.
|
||||
create_context_editing_middleware(),
|
||||
create_runtime_context_middleware(),
|
||||
ToolErrorHandlerMiddleware(),
|
||||
ContextOverflowMapperMiddleware(),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _build_prompt_refs() -> dict:
|
||||
"""Build prompt references with the current date (not frozen at import)."""
|
||||
return {
|
||||
"RESEARCHER_INSTRUCTIONS": RESEARCHER_INSTRUCTIONS.format(
|
||||
date=datetime.now().strftime("%Y-%m-%d"),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _maybe_swap_async_subagents(subs: list, middleware: list | None = None) -> list:
|
||||
"""Replace ``_async``-flagged sub-agents with ``AsyncSubAgent`` specs when enabled.
|
||||
|
||||
@@ -333,7 +325,6 @@ def _build_base_kwargs(base_backend, base_middleware):
|
||||
subs = load_subagents(
|
||||
SUBAGENTS_CONFIG,
|
||||
tool_registry=tool_registry,
|
||||
prompt_refs=_build_prompt_refs(),
|
||||
)
|
||||
_inject_subagent_middleware(subs)
|
||||
subs = _maybe_swap_async_subagents(subs, base_middleware)
|
||||
@@ -382,7 +373,6 @@ def load_mcp_and_build_kwargs(base_backend, base_middleware, *, on_mcp_progress=
|
||||
subs = load_subagents(
|
||||
SUBAGENTS_CONFIG,
|
||||
tool_registry=registry,
|
||||
prompt_refs=_build_prompt_refs(),
|
||||
)
|
||||
|
||||
_inject_subagent_middleware(subs)
|
||||
@@ -476,6 +466,7 @@ def _get_default_middleware(
|
||||
create_code_interpreter_middleware,
|
||||
create_context_editing_middleware,
|
||||
create_memory_middleware,
|
||||
create_runtime_context_middleware,
|
||||
create_tool_selector_middleware,
|
||||
load_fallback_chain,
|
||||
)
|
||||
@@ -496,6 +487,7 @@ def _get_default_middleware(
|
||||
ContextOverflowMapperMiddleware(),
|
||||
ToolErrorHandlerMiddleware(),
|
||||
*create_tool_selector_middleware(model=model),
|
||||
create_runtime_context_middleware(),
|
||||
create_memory_middleware(memory_dir, workspace_dir=workspace_dir),
|
||||
]
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ _EXPORTS: dict[str, tuple[str, str]] = {
|
||||
"DEFAULT_MODEL": (".llm", "DEFAULT_MODEL"),
|
||||
# Prompts
|
||||
"get_system_prompt": (".prompts", "get_system_prompt"),
|
||||
"RESEARCHER_INSTRUCTIONS": (".prompts", "RESEARCHER_INSTRUCTIONS"),
|
||||
# Tools
|
||||
"tavily_search": (".tools", "tavily_search"),
|
||||
"think_tool": (".tools", "think_tool"),
|
||||
|
||||
@@ -23,6 +23,7 @@ from .memory import (
|
||||
create_memory_middleware,
|
||||
)
|
||||
from .model_fallback import ModelFallbackMiddleware, load_fallback_chain
|
||||
from .runtime_context import RuntimeContextMiddleware, create_runtime_context_middleware
|
||||
from .tool_error_handler import ToolErrorHandlerMiddleware
|
||||
from .tool_selector import create_tool_selector_middleware
|
||||
from .utils import disable_thinking
|
||||
@@ -37,11 +38,13 @@ __all__ = [
|
||||
"EvoMemoryMiddleware",
|
||||
"ModelFallbackMiddleware",
|
||||
"Question",
|
||||
"RuntimeContextMiddleware",
|
||||
"ToolErrorHandlerMiddleware",
|
||||
"compute_context_editing_trigger",
|
||||
"create_code_interpreter_middleware",
|
||||
"create_context_editing_middleware",
|
||||
"create_memory_middleware",
|
||||
"create_runtime_context_middleware",
|
||||
"create_tool_selector_middleware",
|
||||
"disable_thinking",
|
||||
"load_fallback_chain",
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Runtime context middleware for EvoScientist."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import datetime
|
||||
|
||||
from langchain.agents.middleware.types import (
|
||||
AgentMiddleware,
|
||||
ModelRequest,
|
||||
ModelResponse,
|
||||
)
|
||||
|
||||
RUNTIME_CONTEXT_TEMPLATE = """<runtime_context>
|
||||
Current date: {date}
|
||||
Local timezone: {timezone}
|
||||
|
||||
Use this context to resolve relative time references like today, yesterday, and
|
||||
next week.
|
||||
</runtime_context>"""
|
||||
|
||||
|
||||
def _format_timezone(now: datetime) -> str:
|
||||
"""Return a compact local timezone label for prompt injection."""
|
||||
offset = now.utcoffset()
|
||||
if offset is None:
|
||||
return "local"
|
||||
|
||||
total_seconds = int(offset.total_seconds())
|
||||
sign = "+" if total_seconds >= 0 else "-"
|
||||
total_seconds = abs(total_seconds)
|
||||
hours, remainder = divmod(total_seconds, 3600)
|
||||
minutes = remainder // 60
|
||||
offset_text = f"UTC{sign}{hours:02d}:{minutes:02d}"
|
||||
|
||||
name = now.tzname()
|
||||
if name and name != offset_text:
|
||||
return f"{name} ({offset_text})"
|
||||
return offset_text
|
||||
|
||||
|
||||
class RuntimeContextMiddleware(AgentMiddleware):
|
||||
"""Inject per-turn runtime context into model calls."""
|
||||
|
||||
def __init__(self, *, now_fn: Callable[[], datetime] | None = None) -> None:
|
||||
self._now_fn = now_fn or (lambda: datetime.now().astimezone())
|
||||
|
||||
def _runtime_context(self) -> str:
|
||||
now = self._now_fn()
|
||||
return RUNTIME_CONTEXT_TEMPLATE.format(
|
||||
date=now.strftime("%Y-%m-%d"),
|
||||
timezone=_format_timezone(now),
|
||||
)
|
||||
|
||||
def modify_request(self, request: ModelRequest) -> ModelRequest:
|
||||
"""Append runtime context to the system prompt."""
|
||||
from deepagents.middleware._utils import append_to_system_message
|
||||
|
||||
new_system = append_to_system_message(
|
||||
request.system_message,
|
||||
self._runtime_context(),
|
||||
)
|
||||
return request.override(system_message=new_system)
|
||||
|
||||
def wrap_model_call(
|
||||
self,
|
||||
request: ModelRequest,
|
||||
handler: Callable[[ModelRequest], ModelResponse],
|
||||
) -> ModelResponse:
|
||||
"""Inject runtime context before the sync model handler."""
|
||||
return handler(self.modify_request(request))
|
||||
|
||||
async def awrap_model_call(
|
||||
self,
|
||||
request: ModelRequest,
|
||||
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
||||
) -> ModelResponse:
|
||||
"""Inject runtime context before the async model handler."""
|
||||
return await handler(self.modify_request(request))
|
||||
|
||||
|
||||
def create_runtime_context_middleware(
|
||||
*, now_fn: Callable[[], datetime] | None = None
|
||||
) -> RuntimeContextMiddleware:
|
||||
"""Build runtime-context middleware."""
|
||||
return RuntimeContextMiddleware(now_fn=now_fn)
|
||||
+5
-56
@@ -13,8 +13,7 @@ The main agent's system prompt is assembled by :func:`get_system_prompt` from:
|
||||
- :data:`ASYNC_NOTIFICATIONS` — how to triage `[Async tasks update]` signals
|
||||
from async sub-agents
|
||||
|
||||
:data:`RESEARCHER_INSTRUCTIONS` is the research-agent sub-agent prompt,
|
||||
loaded via ``_build_prompt_refs`` in ``EvoScientist.py``.
|
||||
Built-in sub-agent prompts live in ``EvoScientist/subagents/*.yaml``.
|
||||
|
||||
Style notes
|
||||
-----------
|
||||
@@ -346,56 +345,6 @@ For EACH task in the batch, independently:
|
||||
It is fine to fetch one task and defer another from the same batch.
|
||||
"""
|
||||
|
||||
# =============================================================================
|
||||
# Sub-agent research instructions
|
||||
# =============================================================================
|
||||
|
||||
RESEARCHER_INSTRUCTIONS = """You are a research assistant. Today's date is {date}.
|
||||
|
||||
## Task
|
||||
Use tools to gather information on the assigned topic (methods, baselines, datasets, or prior results) to support experimental planning or iteration. Prefer actionable details: datasets, metrics, code availability, and common pitfalls. Do not fabricate citations or URLs. Capture evaluation protocols (splits, metrics, calibration) and known failure modes.
|
||||
|
||||
## Available Tools
|
||||
- `think_tool` — Reflect on findings and plan next steps
|
||||
- `read_file` — Read skill instructions when a skill matches the task (paths shown in your available skills listing)
|
||||
- Optionally, some web search tools to find information online.
|
||||
|
||||
**CRITICAL:** Use `think_tool` after each search
|
||||
|
||||
## Research Strategy
|
||||
1. Read the question carefully
|
||||
2. Start with broad searches
|
||||
3. After each search, reflect: Do I have enough? What's missing?
|
||||
4. Narrow searches to fill gaps
|
||||
5. Stop when you can answer confidently
|
||||
|
||||
## Hard Limits
|
||||
- Simple queries: 2-3 searches maximum
|
||||
- Complex queries: up to 5 searches maximum
|
||||
- Stop after 5 searches regardless
|
||||
|
||||
## Stop When
|
||||
- You can answer comprehensively
|
||||
- You have 3+ relevant sources
|
||||
- Last 2 searches returned similar information
|
||||
|
||||
## Response Format
|
||||
Structure findings with clear headings and cite sources inline:
|
||||
|
||||
```
|
||||
## Key Findings
|
||||
|
||||
Finding one with context [1]. Another insight [2].
|
||||
|
||||
## Recommended Next Experiments
|
||||
- One actionable experiment suggestion with motivation and expected outcome.
|
||||
|
||||
### Sources
|
||||
[1] Title: URL
|
||||
[2] Title: URL
|
||||
```
|
||||
"""
|
||||
|
||||
# =============================================================================
|
||||
# Combined exports
|
||||
# =============================================================================
|
||||
@@ -414,10 +363,10 @@ def get_system_prompt() -> str:
|
||||
6. :data:`DELEGATION_STRATEGY`
|
||||
7. :data:`ASYNC_NOTIFICATIONS`
|
||||
|
||||
The current date is injected per-turn by
|
||||
:class:`EvoScientist.middleware.EvoMemoryMiddleware` (piggy-backing on its
|
||||
existing ``<evo_memory>`` injection), so the static prefix here remains
|
||||
byte-stable across midnight rollover and across long-running daemons.
|
||||
Runtime context is injected per-turn by
|
||||
:class:`EvoScientist.middleware.RuntimeContextMiddleware`, so the static
|
||||
prefix here remains byte-stable across midnight rollover and across
|
||||
long-running daemons.
|
||||
|
||||
Returns:
|
||||
Combined static system prompt string.
|
||||
|
||||
@@ -40,7 +40,6 @@ def build_async_subagent_graph(name: str) -> Any:
|
||||
from EvoScientist.config import apply_config_to_env, get_effective_config
|
||||
from EvoScientist.EvoScientist import (
|
||||
SUBAGENTS_CONFIG,
|
||||
_build_prompt_refs,
|
||||
_ensure_chat_model,
|
||||
_get_default_backend,
|
||||
_get_default_middleware,
|
||||
@@ -63,7 +62,6 @@ def build_async_subagent_graph(name: str) -> Any:
|
||||
specs = load_subagents(
|
||||
SUBAGENTS_CONFIG,
|
||||
tool_registry=tool_registry,
|
||||
prompt_refs=_build_prompt_refs(),
|
||||
)
|
||||
spec = next((s for s in specs if s.get("name") == name), None)
|
||||
if spec is None:
|
||||
|
||||
@@ -2,4 +2,48 @@ research-agent:
|
||||
description: "Web research for methods/baselines/datasets (one topic at a time, return actionable notes + sources)."
|
||||
tools: [tavily_search, think_tool]
|
||||
skills: ["/skills/"]
|
||||
system_prompt_ref: RESEARCHER_INSTRUCTIONS
|
||||
system_prompt: |
|
||||
You are a research assistant.
|
||||
|
||||
## Task
|
||||
Use tools to gather information on the assigned topic (methods, baselines, datasets, or prior results) to support experimental planning or iteration. Prefer actionable details: datasets, metrics, code availability, and common pitfalls. Do not fabricate citations or URLs. Capture evaluation protocols (splits, metrics, calibration) and known failure modes.
|
||||
|
||||
## Available Tools
|
||||
- `think_tool` - Reflect on findings and plan next steps
|
||||
- `read_file` - Read skill instructions when a skill matches the task (paths shown in your available skills listing)
|
||||
- Optionally, some web search tools to find information online.
|
||||
|
||||
**CRITICAL:** Use `think_tool` after each search
|
||||
|
||||
## Research Strategy
|
||||
1. Read the question carefully
|
||||
2. Start with broad searches
|
||||
3. After each search, reflect: Do I have enough? What's missing?
|
||||
4. Narrow searches to fill gaps
|
||||
5. Stop when you can answer confidently
|
||||
|
||||
## Hard Limits
|
||||
- Simple queries: 2-3 searches maximum
|
||||
- Complex queries: up to 5 searches maximum
|
||||
- Stop after 5 searches regardless
|
||||
|
||||
## Stop When
|
||||
- You can answer comprehensively
|
||||
- You have 3+ relevant sources
|
||||
- Last 2 searches returned similar information
|
||||
|
||||
## Response Format
|
||||
Structure findings with clear headings and cite sources inline:
|
||||
|
||||
```
|
||||
## Key Findings
|
||||
|
||||
Finding one with context [1]. Another insight [2].
|
||||
|
||||
## Recommended Next Experiments
|
||||
- One actionable experiment suggestion with motivation and expected outcome.
|
||||
|
||||
### Sources
|
||||
[1] Title: URL
|
||||
[2] Title: URL
|
||||
```
|
||||
|
||||
@@ -137,7 +137,8 @@ def load_subagents(
|
||||
research-agent:
|
||||
description: "..."
|
||||
tools: [tavily_search, think_tool]
|
||||
system_prompt_ref: RESEARCHER_INSTRUCTIONS
|
||||
system_prompt: |
|
||||
...
|
||||
"""
|
||||
prompt_refs = prompt_refs or {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user