Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/services/agent_automation/intent_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ async def analyze(self, context: AutomationIntentContext) -> Dict[str, Any]:

def _generate_sync(self, context: AutomationIntentContext) -> str:
from nexent.core.models import OpenAIModel
from nexent.core.utils.observer import MessageObserver
from utils.config_utils import get_model_name_from_config

language = detect_instruction_language(context.message)
Expand All @@ -289,6 +290,7 @@ def _generate_sync(self, context: AutomationIntentContext) -> str:
undefined=StrictUndefined,
).render(**values).strip()
llm = OpenAIModel(
observer=MessageObserver(),
model_id=get_model_name_from_config(self._model_config),
Comment on lines 292 to 294
api_base=self._model_config.get("base_url", ""),
api_key=self._model_config.get("api_key", ""),
Expand Down
2 changes: 2 additions & 0 deletions backend/services/agent_automation/prompt_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,14 @@ def _generate_sync(
user_key: str,
) -> str:
from nexent.core.models import OpenAIModel
from nexent.core.utils.observer import MessageObserver
from utils.config_utils import get_model_name_from_config

prompt_template = get_prompt_template("agent_automation", context.language)
values = {"instruction": context.instruction.strip()}
user_prompt = Template(prompt_template[user_key], undefined=StrictUndefined).render(**values).strip()
llm = OpenAIModel(
observer=MessageObserver(),
model_id=get_model_name_from_config(self._model_config) if self._model_config.get("model_name") else "",
Comment on lines 236 to 238
api_base=self._model_config.get("base_url", ""),
api_key=self._model_config.get("api_key", ""),
Expand Down
15 changes: 13 additions & 2 deletions backend/services/agent_automation/tool_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@
DEFAULT_AUTOMATION_TIMEZONE = "Asia/Shanghai"


def _strip_runtime_time_prefix(message: str) -> str:
"""Remove the runtime-only current-time header from the user request."""
normalized = str(message or "")
if normalized.startswith("[Current time:"):
close_index = normalized.find("]", len("[Current time:"))
if close_index >= 0:
return normalized[close_index + 1:].lstrip("\n").strip()
return normalized
Comment on lines +40 to +44


def _run_coroutine(coro):
try:
asyncio.get_running_loop()
Expand Down Expand Up @@ -130,7 +140,8 @@ async def create_proposal(
# The model argument is intentionally not forwarded to extraction. The
# persisted current user message is the authoritative business input.
del request_text
language = detect_instruction_language(context.user_message)
user_message = _strip_runtime_time_prefix(context.user_message)
language = detect_instruction_language(user_message)
if context.source_message_id is None:
message = (
"本轮消息尚未完成持久化,无法安全创建定时任务提案。请稍后重试。"
Expand Down Expand Up @@ -160,7 +171,7 @@ async def create_proposal(
request = AutomationProposalCreateRequest(
conversation_id=context.conversation_id,
agent_id=context.agent_id,
message=context.user_message,
message=user_message,
timezone=context.timezone or DEFAULT_AUTOMATION_TIMEZONE,
agent_version_no=context.agent_version_no,
model_id=context.model_id,
Expand Down
5 changes: 5 additions & 0 deletions sdk/nexent/core/agents/core_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ class InvalidActionFormatError(AgentExecutionError):
r"|(?:我(?:将|需要|先)|接下来|下一步|i\s+(?:will|need\s+to|should)\b|next\b).{0,240})"
r"(?:调用|使用|检索|搜索|call|use|search|invoke)"
)
_EXPLICIT_FINAL_ANSWER_RE = re.compile(
r"(?is)(?:^|\n)\s*(?:最终回答|final\s+answer)\s*[::]\s*\S"
)


def _looks_like_invalid_action_output(text: Any) -> bool:
Expand Down Expand Up @@ -263,6 +266,8 @@ def _looks_like_incomplete_action_output(
return True
if _looks_like_invalid_action_output(text):
return True
if _EXPLICIT_FINAL_ANSWER_RE.search(text):
return False

normalized = text.casefold()
mentioned_tool = any(
Expand Down
12 changes: 11 additions & 1 deletion test/backend/services/test_agent_automation_intent_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ def test_llm_analyzer_generate_sync_invokes_model_as_callable(monkeypatch):
"schedule_error": None,
}, ensure_ascii=False)

captured = {}

class FakeModel:
def __init__(self):
self.calls = None
Expand All @@ -229,7 +231,12 @@ def __call__(self, messages):
return SimpleNamespace(content=fake_payload)

fake_model = FakeModel()
monkeypatch.setattr("nexent.core.models.OpenAIModel", lambda **kwargs: fake_model)

def build_fake_model(**kwargs):
captured["config"] = kwargs
return fake_model

monkeypatch.setattr("nexent.core.models.OpenAIModel", build_fake_model)
monkeypatch.setattr(
"services.agent_automation.intent_analyzer.get_prompt_template",
lambda *a, **k: {
Expand All @@ -251,6 +258,9 @@ def __call__(self, messages):
assert result["analysis_source"] == "llm"
assert result["is_automation_intent"] is True
assert result["schedule_trigger"].cron_expr == "0 8 * * *"
from nexent.core.utils.observer import MessageObserver

assert isinstance(captured["config"]["observer"], MessageObserver)
assert fake_model.calls[0] == {"role": "system", "content": "sys"}
assert fake_model.calls[1]["content"].startswith("msg: ")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,9 @@ def __call__(self, messages):
assert result == '{"title":"A","instruction":"B"}'
assert captured["config"]["model_id"] == "resolved-model"
assert captured["config"]["ssl_verify"] is False
from nexent.core.utils.observer import MessageObserver

assert isinstance(captured["config"]["observer"], MessageObserver)
assert captured["messages"][1]["content"] == "Instruction: do work"


Expand Down
21 changes: 20 additions & 1 deletion test/backend/services/test_agent_automation_tool_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from services.agent_automation.tool_adapter import (
AgentLoopAutomationToolAdapter,
AutomationToolRuntimeContext,
_strip_runtime_time_prefix,
link_persisted_proposal_card,
)

Expand Down Expand Up @@ -36,6 +37,24 @@ def test_build_tool_config_registers_scheduled_task_as_builtin(monkeypatch):
assert callable(tool_config.metadata["create_proposal"])


@pytest.mark.parametrize(
("message", "expected"),
[
(
"[Current time: 2026-08-26 09:20:28]\n\n每天早上9点查一下八字信息",
"每天早上9点查一下八字信息",
),
(
"[Current time: 2026-08-26 09:20:28]\n\nEvery day at 9 AM, check the report",
"Every day at 9 AM, check the report",
),
("每天九点生成日报", "每天九点生成日报"),
],
)
def test_strip_runtime_time_prefix_keeps_only_the_original_request(message, expected):
assert _strip_runtime_time_prefix(message) == expected


def test_build_callback_runs_coroutine_without_an_event_loop(monkeypatch):
async def fake_create_proposal(context, request_text):
return {"request_text": request_text, "conversation_id": context.conversation_id}
Expand Down Expand Up @@ -101,7 +120,7 @@ async def fake_create_proposal(request, tenant_id, user_id, **kwargs):
user_id="user-1",
conversation_id=20,
agent_id=7,
user_message="每天九点生成日报",
user_message="[Current time: 2026-08-26 09:20:28]\n\n每天九点生成日报",
source_message_id=101,
model_id=3,
)
Expand Down
21 changes: 21 additions & 0 deletions test/sdk/core/agents/test_core_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,12 +375,33 @@ def test_complete_answer_that_names_tool_is_not_misclassified():
) is False


@pytest.mark.parametrize(
"output",
[
(
"思考:工具调用成功。根据策略,我需要用 `final_answer` 返回工具结果。\n\n"
"最终回答:\n定时任务提案已生成,请核对任务内容和执行时间后确认创建。"
),
(
"Analysis: The tool call succeeded, so I will use `final_answer` to return the result.\n\n"
"Final answer:\nThe scheduled-task proposal is ready for confirmation."
),
],
)
def test_complete_explicit_final_answer_is_not_misclassified(output):
assert core_agent_module._looks_like_incomplete_action_output(
output,
available_tool_names={"final_answer", "create_scheduled_task_proposal"},
) is False


def test_length_truncated_non_code_output_is_not_a_final_answer():
assert core_agent_module._looks_like_incomplete_action_output(
"这是一个尚未完成的回答",
finish_reason="length",
) is True


def test_parse_code_blobs_run_format():
"""Test parse_code_blobs with <code>...</code> pattern (new format)."""
text = """Here is some code:
Expand Down
Loading