refactor(agent): remove memory agent, simplify runtime config system
This commit is contained in:
@@ -6,6 +6,7 @@ import pytest
|
||||
from ag_ui.core import RunAgentInput
|
||||
|
||||
from core.agentscope.runtime.orchestrator import AgentScopeRuntimeOrchestrator
|
||||
from schemas.automation import MemoryContextConfig, RuntimeConfig
|
||||
from schemas.user import UserContext, parse_profile_settings
|
||||
|
||||
|
||||
@@ -42,11 +43,18 @@ def _run_input() -> RunAgentInput:
|
||||
"messages": [{"id": "u1", "role": "user", "content": "hello"}],
|
||||
"tools": [],
|
||||
"context": [],
|
||||
"forwardedProps": {"agent_type": "worker"},
|
||||
"forwardedProps": {"runtime_mode": "automation"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _runtime_config() -> RuntimeConfig:
|
||||
return RuntimeConfig(
|
||||
enabled_tools=[],
|
||||
context=MemoryContextConfig(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_orchestrator_emits_run_lifecycle_events() -> None:
|
||||
pipeline = _FakePipeline()
|
||||
@@ -58,7 +66,7 @@ async def test_orchestrator_emits_run_lifecycle_events() -> None:
|
||||
run_input=_run_input(),
|
||||
context_messages=[],
|
||||
user_context=_user_context(),
|
||||
system_agent_mode="worker",
|
||||
runtime_config=_runtime_config(),
|
||||
)
|
||||
|
||||
assert result["worker"]["answer"] == "done"
|
||||
|
||||
@@ -5,7 +5,6 @@ from ag_ui.core import RunAgentInput
|
||||
|
||||
import core.agentscope.runtime.runner as runner_module
|
||||
from core.agentscope.runtime.runner import AgentScopeRunner
|
||||
from schemas.automation.config import default_memory_job_config
|
||||
from schemas.agent.runtime_models import (
|
||||
ExecutionMode,
|
||||
NormalizedTaskInput,
|
||||
@@ -19,6 +18,7 @@ from schemas.agent.runtime_models import (
|
||||
WorkerAgentOutputLite,
|
||||
)
|
||||
from schemas.agent.system_agent import AgentType
|
||||
from schemas.automation import MemoryContextConfig, RuntimeConfig
|
||||
from schemas.user import UserContext, parse_profile_settings
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ def _run_input() -> RunAgentInput:
|
||||
"messages": [{"id": "u1", "role": "user", "content": "hello"}],
|
||||
"tools": [],
|
||||
"context": [],
|
||||
"forwardedProps": {"agent_type": "worker"},
|
||||
"forwardedProps": {"runtime_mode": "automation"},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -45,10 +45,20 @@ def _user_context() -> UserContext:
|
||||
)
|
||||
|
||||
|
||||
def _runtime_config() -> RuntimeConfig:
|
||||
return RuntimeConfig(
|
||||
enabled_tools=[],
|
||||
context=MemoryContextConfig(),
|
||||
)
|
||||
|
||||
|
||||
def test_build_worker_input_messages_only_contains_router_contract() -> None:
|
||||
runner = AgentScopeRunner()
|
||||
router_output = RouterAgentOutput(
|
||||
normalized_task_input=NormalizedTaskInput(user_text="安排明天会议"),
|
||||
normalized_task_input=NormalizedTaskInput(
|
||||
user_text="安排明天会议",
|
||||
context_summary="用户询问天气",
|
||||
),
|
||||
key_entities=[],
|
||||
constraints=[],
|
||||
task_typing=TaskTyping(primary=TaskType.SCHEDULING),
|
||||
@@ -67,6 +77,43 @@ def test_build_worker_input_messages_only_contains_router_contract() -> None:
|
||||
assert "[RouterAgentOutput]" in str(input_messages[0].content)
|
||||
|
||||
|
||||
def test_build_router_messages_injects_user_input_when_context_last_not_user() -> None:
|
||||
runner = AgentScopeRunner()
|
||||
run_input = _run_input()
|
||||
|
||||
messages = runner._build_router_messages(
|
||||
context_messages=[],
|
||||
run_input=run_input,
|
||||
)
|
||||
|
||||
assert len(messages) == 1
|
||||
assert messages[0].role == "user"
|
||||
assert messages[0].content == "hello"
|
||||
|
||||
|
||||
def test_build_router_messages_skips_injection_when_context_last_is_user() -> None:
|
||||
runner = AgentScopeRunner()
|
||||
run_input = _run_input()
|
||||
|
||||
from agentscope.message import Msg
|
||||
|
||||
existing_context = [
|
||||
Msg(name="user", role="user", content="之前的问题"),
|
||||
Msg(name="assistant", role="assistant", content="回答"),
|
||||
Msg(name="user", role="user", content="最新用户消息"),
|
||||
]
|
||||
|
||||
messages = runner._build_router_messages(
|
||||
context_messages=existing_context,
|
||||
run_input=run_input,
|
||||
)
|
||||
|
||||
assert len(messages) == len(existing_context)
|
||||
for i, msg in enumerate(messages):
|
||||
assert msg.role == existing_context[i].role
|
||||
assert msg.content == existing_context[i].content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_runtime_client_time_from_forwarded_props() -> None:
|
||||
runner = AgentScopeRunner()
|
||||
@@ -79,7 +126,7 @@ async def test_resolve_runtime_client_time_from_forwarded_props() -> None:
|
||||
"tools": [],
|
||||
"context": [],
|
||||
"forwardedProps": {
|
||||
"agent_type": "worker",
|
||||
"runtime_mode": "automation",
|
||||
"client_time": {
|
||||
"device_timezone": "America/Los_Angeles",
|
||||
"client_now_iso": "2026-03-16T09:12:33-07:00",
|
||||
@@ -95,7 +142,7 @@ async def test_resolve_runtime_client_time_from_forwarded_props() -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_worker_mode_runs_router_then_worker(
|
||||
async def test_execute_runs_router_then_worker(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
class _FakePipeline:
|
||||
@@ -127,7 +174,10 @@ async def test_execute_worker_mode_runs_router_then_worker(
|
||||
async def _fake_execute_router_step(**kwargs: object) -> RouterAgentOutput:
|
||||
del kwargs
|
||||
return RouterAgentOutput(
|
||||
normalized_task_input=NormalizedTaskInput(user_text="安排会议"),
|
||||
normalized_task_input=NormalizedTaskInput(
|
||||
user_text="安排会议",
|
||||
context_summary="用户询问天气",
|
||||
),
|
||||
key_entities=[],
|
||||
constraints=[],
|
||||
task_typing=TaskTyping(primary=TaskType.SCHEDULING),
|
||||
@@ -145,7 +195,7 @@ async def test_execute_worker_mode_runs_router_then_worker(
|
||||
|
||||
monkeypatch.setattr(runner_module, "AsyncSessionLocal", lambda: _FakeSessionCtx())
|
||||
monkeypatch.setattr(runner, "_load_stage_config", _fake_load_stage_config)
|
||||
monkeypatch.setattr(runner, "_build_stage_toolkit", lambda **kwargs: object())
|
||||
monkeypatch.setattr(runner, "_build_toolkit", lambda **kwargs: object())
|
||||
monkeypatch.setattr(runner, "_execute_router_step", _fake_execute_router_step)
|
||||
monkeypatch.setattr(runner, "_execute_worker_step", _fake_execute_worker_step)
|
||||
|
||||
@@ -154,84 +204,9 @@ async def test_execute_worker_mode_runs_router_then_worker(
|
||||
context_messages=[],
|
||||
pipeline=_FakePipeline(),
|
||||
run_input=_run_input(),
|
||||
system_agent_mode="worker",
|
||||
runtime_config=_runtime_config(),
|
||||
)
|
||||
|
||||
assert load_calls == [AgentType.ROUTER, AgentType.WORKER]
|
||||
assert result["router"]["normalized_task_input"]["user_text"] == "安排会议"
|
||||
assert result["worker"]["answer"] == "ok"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_memory_mode_requires_memory_job_config() -> None:
|
||||
runner = AgentScopeRunner()
|
||||
|
||||
class _FakePipeline:
|
||||
async def emit(self, *, session_id: str, event: dict[str, object]) -> str:
|
||||
del session_id, event
|
||||
return "1-0"
|
||||
|
||||
with pytest.raises(RuntimeError, match="memory job config is required"):
|
||||
await runner.execute(
|
||||
user_context=_user_context(),
|
||||
context_messages=[],
|
||||
pipeline=_FakePipeline(),
|
||||
run_input=_run_input(),
|
||||
system_agent_mode="memory",
|
||||
memory_job_config=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_memory_mode_uses_memory_job_config(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
class _FakePipeline:
|
||||
async def emit(self, *, session_id: str, event: dict[str, object]) -> str:
|
||||
del session_id, event
|
||||
return "1-0"
|
||||
|
||||
class _FakeSessionCtx:
|
||||
async def __aenter__(self) -> object:
|
||||
return object()
|
||||
|
||||
async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
|
||||
del exc_type, exc, tb
|
||||
|
||||
runner = AgentScopeRunner()
|
||||
|
||||
async def _fake_build_memory_stage_config(**kwargs: object):
|
||||
del kwargs
|
||||
return runner_module.SystemAgentRuntimeConfig(
|
||||
agent_type=AgentType.MEMORY,
|
||||
model_code="qwen3.5-flash",
|
||||
api_base_url="https://example.com",
|
||||
api_key="test",
|
||||
llm_config=runner_module.SystemAgentLLMConfig(),
|
||||
)
|
||||
|
||||
async def _fake_execute_single_stage_step(**kwargs: object):
|
||||
del kwargs
|
||||
return runner_module.AgentOutput(answer="memory")
|
||||
|
||||
monkeypatch.setattr(runner_module, "AsyncSessionLocal", lambda: _FakeSessionCtx())
|
||||
monkeypatch.setattr(
|
||||
runner, "_build_memory_stage_config", _fake_build_memory_stage_config
|
||||
)
|
||||
monkeypatch.setattr(runner, "_build_stage_toolkit", lambda **kwargs: object())
|
||||
monkeypatch.setattr(
|
||||
runner,
|
||||
"_execute_single_stage_step",
|
||||
_fake_execute_single_stage_step,
|
||||
)
|
||||
|
||||
result = await runner.execute(
|
||||
user_context=_user_context(),
|
||||
context_messages=[],
|
||||
pipeline=_FakePipeline(),
|
||||
run_input=_run_input(),
|
||||
system_agent_mode="memory",
|
||||
memory_job_config=default_memory_job_config(),
|
||||
)
|
||||
|
||||
assert result["memory"]["answer"] == "memory"
|
||||
|
||||
@@ -7,6 +7,7 @@ import pytest
|
||||
|
||||
import core.agentscope.runtime.tasks as tasks_module
|
||||
from schemas.agent import ToolStatus
|
||||
from schemas.automation import ContextWindowMode, MemoryContextConfig
|
||||
from schemas.user import UserContext, parse_profile_settings
|
||||
|
||||
|
||||
@@ -15,21 +16,36 @@ def _run_input_payload() -> dict[str, Any]:
|
||||
"threadId": str(uuid4()),
|
||||
"runId": "run-1",
|
||||
"state": {},
|
||||
"messages": [],
|
||||
"messages": [{"id": "u1", "role": "user", "content": "现在几点"}],
|
||||
"tools": [],
|
||||
"context": [],
|
||||
"forwardedProps": {"agent_type": "worker"},
|
||||
"forwardedProps": {"runtime_mode": "automation"},
|
||||
}
|
||||
|
||||
|
||||
class _FakeSessionCtx:
|
||||
async def __aenter__(self) -> object:
|
||||
return object()
|
||||
async def __aenter__(self) -> "_FakeSession":
|
||||
return _FakeSession()
|
||||
|
||||
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
||||
del exc_type, exc, tb
|
||||
|
||||
|
||||
class _FakeSession:
|
||||
async def execute(self, stmt: object) -> object:
|
||||
del stmt
|
||||
|
||||
class FakeResult:
|
||||
def scalars(self) -> object:
|
||||
class FakeScalars:
|
||||
def all(self) -> list[object]:
|
||||
return []
|
||||
|
||||
return FakeScalars()
|
||||
|
||||
return FakeResult()
|
||||
|
||||
|
||||
async def _fake_user_context(**kwargs: object) -> UserContext:
|
||||
del kwargs
|
||||
return UserContext(
|
||||
@@ -81,6 +97,10 @@ async def test_run_agentscope_task_calls_runtime_run(
|
||||
"command": "run",
|
||||
"owner_id": str(uuid4()),
|
||||
"run_input": _run_input_payload(),
|
||||
"runtime_config": {
|
||||
"enabled_tools": [],
|
||||
"context": {"window_mode": "day", "window_count": 2},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -89,34 +109,28 @@ async def test_run_agentscope_task_calls_runtime_run(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_agentscope_task_includes_recent_context_messages(
|
||||
async def test_run_agentscope_task_injects_runtime_config(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
captured_messages: list[dict[str, Any]] = []
|
||||
captured_config: dict[str, Any] = {}
|
||||
|
||||
class _FakeRuntime:
|
||||
def __init__(self, **kwargs: object) -> None:
|
||||
del kwargs
|
||||
|
||||
async def run(self, **kwargs: object) -> object:
|
||||
raw_context_messages = kwargs.get("context_messages")
|
||||
raw_run_input = kwargs.get("run_input")
|
||||
if isinstance(raw_context_messages, list):
|
||||
captured_messages.extend(raw_context_messages)
|
||||
if raw_run_input is not None:
|
||||
raw_messages = getattr(raw_run_input, "messages", [])
|
||||
if isinstance(raw_messages, list):
|
||||
captured_messages.extend(raw_messages)
|
||||
captured_config.update(
|
||||
{
|
||||
"runtime_config": kwargs.get("runtime_config"),
|
||||
"context_messages": kwargs.get("context_messages"),
|
||||
}
|
||||
)
|
||||
return object()
|
||||
|
||||
async def _fake_get_redis_client() -> object:
|
||||
return object()
|
||||
|
||||
async def _empty_context(**kwargs: object) -> list[dict[str, Any]]:
|
||||
del kwargs
|
||||
return []
|
||||
|
||||
async def _fake_context(**kwargs: object) -> list[dict[str, Any]]:
|
||||
del kwargs
|
||||
return [{"id": "ctx-1", "role": "assistant", "content": "历史上下文"}]
|
||||
|
||||
@@ -133,25 +147,23 @@ async def test_run_agentscope_task_includes_recent_context_messages(
|
||||
"_build_recent_context_messages",
|
||||
_empty_context,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
tasks_module,
|
||||
"_build_recent_context_messages",
|
||||
_fake_context,
|
||||
)
|
||||
|
||||
run_input = _run_input_payload()
|
||||
run_input["messages"] = [{"id": "u1", "role": "user", "content": "现在几点"}]
|
||||
await tasks_module.run_agentscope_task(
|
||||
{
|
||||
"command": "run",
|
||||
"owner_id": str(uuid4()),
|
||||
"run_input": run_input,
|
||||
"run_input": _run_input_payload(),
|
||||
"runtime_config": {
|
||||
"enabled_tools": [],
|
||||
"context": {"window_mode": "day", "window_count": 2},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert len(captured_messages) == 2
|
||||
assert captured_messages[0]["id"] == "ctx-1"
|
||||
assert getattr(captured_messages[1], "id", None) == "u1"
|
||||
assert captured_config["context_messages"] == [
|
||||
{"id": "ctx-1", "role": "assistant", "content": "历史上下文"}
|
||||
]
|
||||
assert captured_config["runtime_config"] is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -177,38 +189,6 @@ async def test_run_agentscope_task_rejects_invalid_command_type() -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_agentscope_task_requires_forwarded_props_agent_type() -> None:
|
||||
payload = _run_input_payload()
|
||||
payload["forwardedProps"] = {}
|
||||
|
||||
with pytest.raises(ValueError, match="invalid RunAgentInput.forwardedProps"):
|
||||
await tasks_module.run_agentscope_task(
|
||||
{
|
||||
"command": "run",
|
||||
"owner_id": str(uuid4()),
|
||||
"run_input": payload,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_agentscope_task_memory_mode_requires_automation_job_id() -> None:
|
||||
payload = _run_input_payload()
|
||||
payload["forwardedProps"] = {"agent_type": "memory"}
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="automation_job_id is required for memory mode"
|
||||
):
|
||||
await tasks_module.run_agentscope_task(
|
||||
{
|
||||
"command": "run",
|
||||
"owner_id": str(uuid4()),
|
||||
"run_input": payload,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_recent_context_messages_includes_all_user_attachments(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -221,9 +201,9 @@ async def test_build_recent_context_messages_includes_all_user_attachments(
|
||||
self,
|
||||
*,
|
||||
thread_id: str,
|
||||
system_agent_mode: str,
|
||||
context_config: MemoryContextConfig,
|
||||
) -> dict[str, object] | None:
|
||||
del thread_id, system_agent_mode
|
||||
del thread_id, context_config
|
||||
return {
|
||||
"messages": [
|
||||
{
|
||||
@@ -257,7 +237,10 @@ async def test_build_recent_context_messages_includes_all_user_attachments(
|
||||
messages = await tasks_module._build_recent_context_messages(
|
||||
session=object(),
|
||||
thread_id=str(uuid4()),
|
||||
context_mode="worker",
|
||||
context_config=MemoryContextConfig(
|
||||
window_mode=ContextWindowMode.DAY,
|
||||
window_count=2,
|
||||
),
|
||||
)
|
||||
|
||||
assert len(messages) == 1
|
||||
@@ -281,9 +264,9 @@ async def test_build_recent_context_messages_uses_tool_metadata_output(
|
||||
self,
|
||||
*,
|
||||
thread_id: str,
|
||||
system_agent_mode: str,
|
||||
context_config: MemoryContextConfig,
|
||||
) -> dict[str, object] | None:
|
||||
del thread_id, system_agent_mode
|
||||
del thread_id, context_config
|
||||
return {
|
||||
"messages": [
|
||||
{
|
||||
@@ -312,7 +295,7 @@ async def test_build_recent_context_messages_uses_tool_metadata_output(
|
||||
messages = await tasks_module._build_recent_context_messages(
|
||||
session=object(),
|
||||
thread_id=str(uuid4()),
|
||||
context_mode="worker",
|
||||
context_config=MemoryContextConfig(),
|
||||
)
|
||||
|
||||
assert len(messages) == 1
|
||||
@@ -336,9 +319,9 @@ async def test_build_recent_context_messages_skips_tool_without_metadata_output(
|
||||
self,
|
||||
*,
|
||||
thread_id: str,
|
||||
system_agent_mode: str,
|
||||
context_config: MemoryContextConfig,
|
||||
) -> dict[str, object] | None:
|
||||
del thread_id, system_agent_mode
|
||||
del thread_id, context_config
|
||||
return {
|
||||
"messages": [
|
||||
{
|
||||
@@ -354,17 +337,17 @@ async def test_build_recent_context_messages_skips_tool_without_metadata_output(
|
||||
messages = await tasks_module._build_recent_context_messages(
|
||||
session=object(),
|
||||
thread_id=str(uuid4()),
|
||||
context_mode="worker",
|
||||
context_config=MemoryContextConfig(),
|
||||
)
|
||||
|
||||
assert messages == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_recent_context_messages_passes_context_mode_through(
|
||||
async def test_build_recent_context_messages_passes_context_config(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
captured_mode: dict[str, str | None] = {"mode": None}
|
||||
captured_config: dict[str, Any] = {"config": None}
|
||||
|
||||
class _FakeContextService:
|
||||
def __init__(self, *, repository: object) -> None:
|
||||
@@ -374,19 +357,21 @@ async def test_build_recent_context_messages_passes_context_mode_through(
|
||||
self,
|
||||
*,
|
||||
thread_id: str,
|
||||
system_agent_mode: str,
|
||||
context_config: MemoryContextConfig,
|
||||
) -> dict[str, object] | None:
|
||||
del thread_id
|
||||
captured_mode["mode"] = system_agent_mode
|
||||
captured_config["config"] = context_config
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(tasks_module, "AgentContextService", _FakeContextService)
|
||||
|
||||
cfg = MemoryContextConfig(window_mode=ContextWindowMode.NUMBER, window_count=10)
|
||||
messages = await tasks_module._build_recent_context_messages(
|
||||
session=object(),
|
||||
thread_id=str(uuid4()),
|
||||
context_mode="worker",
|
||||
context_config=cfg,
|
||||
)
|
||||
|
||||
assert messages == []
|
||||
assert captured_mode["mode"] == "worker"
|
||||
assert captured_config["config"].window_mode == ContextWindowMode.NUMBER
|
||||
assert captured_config["config"].window_count == 10
|
||||
|
||||
@@ -23,19 +23,17 @@ def test_build_agent_prompt_for_worker_contains_runtime_config() -> None:
|
||||
assert "enabled_tools=calendar.read,calendar.write" in prompt
|
||||
|
||||
|
||||
def test_build_agent_prompt_for_memory_uses_memory_rules() -> None:
|
||||
def test_build_agent_prompt_for_router_contains_task_typing_rules() -> None:
|
||||
prompt = build_agent_prompt(
|
||||
agent_type=AgentType.MEMORY,
|
||||
agent_type=AgentType.ROUTER,
|
||||
llm_config=SystemAgentLLMConfig.model_validate(
|
||||
{
|
||||
"context_messages": {"mode": "day", "count": 2},
|
||||
"enabled_tools": ["user.lookup"],
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
assert "- type: memory" in prompt
|
||||
assert "[Memory Agent]" in prompt
|
||||
assert "- type: router" in prompt
|
||||
assert "[Router Agent]" in prompt
|
||||
assert "context_messages.mode=day" in prompt
|
||||
assert "context_messages.count=2" in prompt
|
||||
assert "enabled_tools=user.lookup" in prompt
|
||||
|
||||
@@ -156,3 +156,48 @@ def test_build_system_prompt_keeps_sections_focused_without_language_duplication
|
||||
assert "[Answer Style]" in prompt
|
||||
assert "Default reply language:" not in prompt
|
||||
assert "Follow agent contracts strictly" not in prompt
|
||||
|
||||
|
||||
def test_build_system_prompt_includes_memory_section_when_memories_provided() -> None:
|
||||
from schemas.memories import (
|
||||
MemoryContext,
|
||||
MemoryListResponse,
|
||||
MemorySource,
|
||||
MemoryType,
|
||||
)
|
||||
|
||||
memories = MemoryListResponse(
|
||||
owner_id=uuid4(),
|
||||
memories=[
|
||||
MemoryContext(
|
||||
memory_type=MemoryType.USER,
|
||||
source=MemorySource.MANUAL,
|
||||
title="User prefers morning meetings",
|
||||
content={"text": "User likes meetings before 10am"},
|
||||
created_at=datetime(2026, 3, 1, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2026, 3, 1, tzinfo=timezone.utc),
|
||||
),
|
||||
],
|
||||
total=1,
|
||||
)
|
||||
|
||||
prompt = build_system_prompt(
|
||||
agent_type=AgentType.WORKER,
|
||||
user_context=_build_user_context(),
|
||||
now_utc=datetime(2026, 3, 11, 0, 0, tzinfo=timezone.utc),
|
||||
memories=memories,
|
||||
)
|
||||
|
||||
assert "<!-- MEMORY_START -->" in prompt
|
||||
assert "[User Memories]" in prompt
|
||||
assert "User prefers morning meetings" in prompt
|
||||
|
||||
|
||||
def test_build_system_prompt_omits_memory_section_when_no_memories() -> None:
|
||||
prompt = build_system_prompt(
|
||||
agent_type=AgentType.WORKER,
|
||||
user_context=_build_user_context(),
|
||||
now_utc=datetime(2026, 3, 11, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
assert "<!-- MEMORY_START -->" not in prompt
|
||||
|
||||
@@ -5,17 +5,23 @@ from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from core.automation.scheduler import (
|
||||
AutomationSchedulerService,
|
||||
_compute_next_run_at,
|
||||
from models.automation_jobs import AutomationJob as OrmAutomationJob, ScheduleType
|
||||
from schemas.automation import (
|
||||
RuntimeConfig,
|
||||
)
|
||||
from models.automation_jobs import ScheduleType
|
||||
from schemas.automation.config import AutomationJobConfig
|
||||
from schemas.automation.scheduler import DueAutomationJob
|
||||
from v1.automation_jobs.service import AutomationJobsService, _compute_next_run_at
|
||||
|
||||
|
||||
class _FakeSession:
|
||||
async def commit(self) -> None:
|
||||
pass
|
||||
|
||||
async def rollback(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class _FakeRepository:
|
||||
def __init__(self, jobs: list[DueAutomationJob]) -> None:
|
||||
def __init__(self, jobs: list[OrmAutomationJob]) -> None:
|
||||
self.jobs = jobs
|
||||
self.marked: list[tuple[UUID, datetime, datetime]] = []
|
||||
self.commits = 0
|
||||
@@ -23,30 +29,14 @@ class _FakeRepository:
|
||||
|
||||
async def list_due_jobs(
|
||||
self, *, now_utc: datetime, limit: int
|
||||
) -> list[DueAutomationJob]:
|
||||
) -> list[OrmAutomationJob]:
|
||||
del now_utc
|
||||
return self.jobs[:limit]
|
||||
|
||||
async def get_job_config(self, *, job_id: UUID) -> AutomationJobConfig:
|
||||
del job_id
|
||||
return AutomationJobConfig.model_validate(
|
||||
{
|
||||
"agent_type": "memory",
|
||||
"model_code": "qwen3.5-flash",
|
||||
"enabled_tools": ["calendar.read", "user.lookup"],
|
||||
"input_template": "auto input",
|
||||
"context": {
|
||||
"source": "latest_chat",
|
||||
"window_mode": "day",
|
||||
"window_count": 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
async def ensure_latest_chat_session(self, *, owner_id: UUID) -> UUID:
|
||||
async def get_or_create_chat_session(self, *, owner_id: UUID) -> UUID:
|
||||
return owner_id
|
||||
|
||||
async def mark_job_dispatched(
|
||||
async def update_job_schedule(
|
||||
self,
|
||||
*,
|
||||
job_id: UUID,
|
||||
@@ -55,57 +45,65 @@ class _FakeRepository:
|
||||
) -> None:
|
||||
self.marked.append((job_id, next_run_at, last_run_at))
|
||||
|
||||
async def commit(self) -> None:
|
||||
self.commits += 1
|
||||
|
||||
async def rollback(self) -> None:
|
||||
self.rollbacks += 1
|
||||
|
||||
|
||||
class _FakeQueue:
|
||||
def __init__(self) -> None:
|
||||
self.commands: list[dict[str, object]] = []
|
||||
|
||||
async def enqueue(
|
||||
self,
|
||||
*,
|
||||
command: dict[str, object],
|
||||
dedup_key: str | None,
|
||||
) -> str:
|
||||
del dedup_key
|
||||
self.commands.append(command)
|
||||
return "task-1"
|
||||
def _make_orm_job(
|
||||
*,
|
||||
job_id: UUID | None = None,
|
||||
owner_id: UUID | None = None,
|
||||
schedule_type: ScheduleType = ScheduleType.DAILY,
|
||||
next_run_at: datetime | None = None,
|
||||
) -> OrmAutomationJob:
|
||||
now = datetime(2026, 3, 19, 12, 0, tzinfo=timezone.utc)
|
||||
return OrmAutomationJob(
|
||||
id=job_id or uuid4(),
|
||||
owner_id=owner_id or uuid4(),
|
||||
title="Test Job",
|
||||
config={
|
||||
"enabled_tools": ["calendar.read", "user.lookup"],
|
||||
"context": {
|
||||
"source": "latest_chat",
|
||||
"window_mode": "day",
|
||||
"window_count": 2,
|
||||
},
|
||||
"input_template": "auto input: {date}",
|
||||
},
|
||||
schedule_type=schedule_type,
|
||||
run_at=now - timedelta(hours=1),
|
||||
next_run_at=next_run_at or now - timedelta(minutes=1),
|
||||
timezone="UTC",
|
||||
last_run_at=None,
|
||||
status="active",
|
||||
created_by=None,
|
||||
created_at=now - timedelta(days=1),
|
||||
updated_at=now - timedelta(hours=1),
|
||||
deleted_at=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scan_and_dispatch_enqueues_memory_run_command() -> None:
|
||||
async def test_scan_and_dispatch_calls_dispatch_fn_with_runtime_config() -> None:
|
||||
now = datetime(2026, 3, 19, 12, 0, tzinfo=timezone.utc)
|
||||
owner_id = uuid4()
|
||||
job_id = uuid4()
|
||||
repo = _FakeRepository(
|
||||
jobs=[
|
||||
DueAutomationJob(
|
||||
id=job_id,
|
||||
owner_id=owner_id,
|
||||
schedule_type=ScheduleType.DAILY,
|
||||
timezone="UTC",
|
||||
next_run_at=now - timedelta(minutes=1),
|
||||
)
|
||||
]
|
||||
)
|
||||
queue = _FakeQueue()
|
||||
service = AutomationSchedulerService(repository=repo, queue=queue)
|
||||
repo = _FakeRepository(jobs=[_make_orm_job(job_id=job_id, owner_id=owner_id)])
|
||||
dispatched_calls: list[dict] = []
|
||||
|
||||
result = await service.scan_and_dispatch(now_utc=now, limit=10)
|
||||
async def dispatch_fn(**kwargs: object) -> None:
|
||||
dispatched_calls.append(kwargs)
|
||||
|
||||
service = AutomationJobsService(repository=repo, session=_FakeSession())
|
||||
|
||||
result = await service.scan_and_dispatch(
|
||||
now_utc=now, limit=10, dispatch_fn=dispatch_fn
|
||||
)
|
||||
|
||||
assert result.scanned == 1
|
||||
assert result.dispatched == 1
|
||||
assert len(queue.commands) == 1
|
||||
run_input = queue.commands[0]["run_input"]
|
||||
assert isinstance(run_input, dict)
|
||||
assert run_input["forwardedProps"] == {"agent_type": "memory"}
|
||||
assert queue.commands[0]["automation_job_id"] == str(job_id)
|
||||
assert repo.commits == 1
|
||||
assert len(dispatched_calls) == 1
|
||||
assert dispatched_calls[0]["owner_id"] == owner_id
|
||||
assert dispatched_calls[0]["runtime_config"] is not None
|
||||
cfg: RuntimeConfig = dispatched_calls[0]["runtime_config"]
|
||||
assert len(cfg.enabled_tools) == 2
|
||||
|
||||
|
||||
def test_compute_next_run_at_daily() -> None:
|
||||
|
||||
Reference in New Issue
Block a user