feat:合并no_reply和wait
This commit is contained in:
@@ -78,6 +78,16 @@ function unwrapModelConfig(data: unknown): Record<string, unknown> {
|
||||
return data as Record<string, unknown>
|
||||
}
|
||||
|
||||
function getAdvancedTaskNames(schema: ConfigSchema | null): Set<string> {
|
||||
const advancedTaskNames = new Set(
|
||||
(schema?.fields ?? [])
|
||||
.filter((field) => field.advanced)
|
||||
.map((field) => field.name)
|
||||
)
|
||||
advancedTaskNames.add('learner')
|
||||
return advancedTaskNames
|
||||
}
|
||||
|
||||
// 主导出组件:包装 RestartProvider
|
||||
export function ModelConfigPage() {
|
||||
return (
|
||||
@@ -174,10 +184,15 @@ function ModelConfigPageContent() {
|
||||
})
|
||||
|
||||
// 检查任务配置问题
|
||||
const checkTaskConfigIssues = useCallback((taskConf: ModelTaskConfig | null, modelList: ModelInfo[]) => {
|
||||
const checkTaskConfigIssues = useCallback((
|
||||
taskConf: ModelTaskConfig | null,
|
||||
modelList: ModelInfo[],
|
||||
schema: ConfigSchema | null = taskConfigSchema
|
||||
) => {
|
||||
if (!taskConf) return
|
||||
|
||||
const modelNameSet = new Set(modelList.map(m => m.name))
|
||||
const advancedTaskNames = getAdvancedTaskNames(schema)
|
||||
const invalidRefs: { taskName: string; invalidModels: string[] }[] = []
|
||||
const emptyTaskList: string[] = []
|
||||
|
||||
@@ -186,7 +201,7 @@ function ModelConfigPageContent() {
|
||||
|
||||
// 检查是否有模型
|
||||
if (!task.model_list || task.model_list.length === 0) {
|
||||
if (key !== 'learner') {
|
||||
if (!advancedTaskNames.has(key)) {
|
||||
emptyTaskList.push(key)
|
||||
}
|
||||
continue
|
||||
@@ -201,7 +216,7 @@ function ModelConfigPageContent() {
|
||||
|
||||
setInvalidModelRefs(invalidRefs)
|
||||
setEmptyTasks(emptyTaskList)
|
||||
}, [])
|
||||
}, [taskConfigSchema])
|
||||
|
||||
// 加载配置
|
||||
const loadConfig = useCallback(async () => {
|
||||
@@ -233,13 +248,15 @@ function ModelConfigPageContent() {
|
||||
resetSnapshots(modelList, taskConf)
|
||||
|
||||
// 解析 model_task_config 的 schema
|
||||
let nextTaskConfigSchema: ConfigSchema | null = null
|
||||
if (schemaResult.success && schemaResult.data) {
|
||||
const schema = (schemaResult.data as unknown as Record<string, unknown>).schema as ConfigSchema
|
||||
setTaskConfigSchema(schema.nested?.model_task_config ?? null)
|
||||
nextTaskConfigSchema = schema.nested?.model_task_config ?? null
|
||||
setTaskConfigSchema(nextTaskConfigSchema)
|
||||
}
|
||||
|
||||
// 检查任务配置问题
|
||||
checkTaskConfigIssues(taskConf, modelList)
|
||||
checkTaskConfigIssues(taskConf, modelList, nextTaskConfigSchema)
|
||||
|
||||
// 初始化上一次的 embedding 模型列表
|
||||
const embeddingModels = taskConf?.embedding?.model_list || []
|
||||
|
||||
@@ -13,18 +13,16 @@ You need to first gather information that can help {bot_name} take the next acti
|
||||
|
||||
|
||||
You can use these tools:
|
||||
- wait(seconds) - Temporarily pause the conversation, wait for `seconds`, hand the speaking turn to the user, and wait for the other party's new message.
|
||||
- no_reply() - When you judge that {bot_name} should not speak right now, end the conversation and do not reply in any way until the other party sends a new message.
|
||||
- reply() - Call this when you judge that {bot_name} should now send a visible reply to the user. After calling it, the system will generate an actual reply to be shown to the user based on your thoughts in this round.
|
||||
- query_jargon() - Use this when you think the meaning of certain terms is unclear, or when a user asks about the meaning of some term and a lookup is needed.
|
||||
- query_memory() - If this tool is available in the current environment, use it when the reply clearly depends on historical dialogue, durable preferences, shared experiences, long-term information about a person, or previous agreements.
|
||||
- Other defined tools may also be used as appropriate.
|
||||
|
||||
Tool usage rules:
|
||||
1. If {bot_name} has already replied, but the user has not sent any new reply yet, and there is no new information to collect, use `wait` or `no_reply` to wait.
|
||||
2. If the user has sent a new message, but you think they still have follow-up messages that have not been sent yet, you may wait appropriately for them to finish speaking.
|
||||
3. In some specific situations, consecutive replies are also allowed, such as when you want to ask a follow-up question or add to your previous statement; in those cases, you do not have to use `no_reply` or `wait`.
|
||||
4. You need to control your speaking frequency. If it is a one-on-one chat, you may speak at a relatively even frequency. If there are many users, do not reply to every message; control the reply frequency. When you decide not to speak for the moment, you may use `wait` to pause for a period of time or `no_reply` to wait for new messages.
|
||||
1. If {bot_name} should send a visible reply now, use `reply`.
|
||||
2. If the user has sent a new message, but you think they still have follow-up messages that have not been sent yet, do not force a reply; end this round with `finish`.
|
||||
3. In some specific situations, consecutive replies are also allowed, such as when you want to ask a follow-up question or add to your previous statement.
|
||||
4. You need to control your speaking frequency. If it is a one-on-one chat, you may speak at a relatively even frequency. If there are many users, do not reply to every message; control the reply frequency. When you decide not to speak for the moment, end this round with `finish`.
|
||||
5. Do not reply to every message. Do not directly reply to sticker-only messages sent by other users. Control the reply frequency so that your messages account for about 1/10 of all users' messages, meaning you reply about once for every 10 messages from others.
|
||||
6. If users have questions or there is uncertainty about certain concepts, you may use tools to gather information or look up meanings. You may use multiple tools.
|
||||
|
||||
|
||||
@@ -12,18 +12,16 @@
|
||||
まず {bot_name} が次の行動を取るのに役立つ情報を集め、そのうえで返信方針を示してください。
|
||||
|
||||
使用できるツール:
|
||||
- wait(seconds) - 会話を一時停止し、`seconds` 秒待って発話権をユーザーに渡し、相手の新しい発言を待ちます。
|
||||
- no_reply() - {bot_name} が今は発言すべきでないと判断した場合、会話を終了し、相手に新しいメッセージが来るまで一切返信しません。
|
||||
- reply() - {bot_name} が今ユーザーに対して可視の返信を送るべきだと判断したときに呼び出します。呼び出し後、システムはこのターンの思考に基づいて、実際にユーザーへ表示される返信を生成します。
|
||||
- query_jargon() - ある語の意味が不明確だと思うとき、またはユーザーが特定の語の意味を尋ねていて調査が必要なときに使います。
|
||||
- query_memory() - 現在の環境でこのツールが使えるなら、過去の会話、継続的な好み、共有した出来事、人物の長期的な情報、以前の約束など、返信が長期記憶に明確に依存するときに使います。
|
||||
- その他定義済みのツールも、状況に応じて使用できます。
|
||||
|
||||
ツール使用ルール:
|
||||
1. {bot_name} がすでに返信済みで、ユーザーからまだ新しい返信がなく、新たに集めるべき情報もない場合は `wait` または `no_reply` を使って待ってください。
|
||||
2. ユーザーに新しい発言があっても、まだ続きの発言が送られていないだけだと判断するなら、適切に待って話し終えるのを待っても構いません。
|
||||
3. 特定の状況では連続返信も可能です。たとえば追問したいときや、自分の直前の発言を補足したいときは、`no_reply` や `wait` を使わなくても構いません。
|
||||
4. 発言頻度は制御してください。一対一の会話なら比較的均等な頻度で発言して構いませんが、ユーザーが多い場合はすべての発言に反応しないでください。しばらく発言しないと決めた場合は、`wait` で一定時間待つか、`no_reply` で新着メッセージを待ってください。
|
||||
1. {bot_name} が今ユーザーに見える返信を送るべきなら、`reply` を使ってください。
|
||||
2. ユーザーに新しい発言があっても、まだ続きの発言が送られていないだけだと判断するなら、無理に返信せず `finish` でこのラウンドを終えてください。
|
||||
3. 特定の状況では連続返信も可能です。たとえば追問したいときや、自分の直前の発言を補足したいときです。
|
||||
4. 発言頻度は制御してください。一対一の会話なら比較的均等な頻度で発言して構いませんが、ユーザーが多い場合はすべての発言に反応しないでください。しばらく発言しないと決めた場合は、`finish` でこのラウンドを終えてください。
|
||||
5. すべてのメッセージに返信しないでください。他ユーザーが送ったスタンプだけのメッセージには直接返信しないでください。返信頻度をコントロールし、自分の発言量は全体のおよそ 1/10 程度、つまり他のユーザーが 10 回ほど発言したら 1 回返信する程度を目安にしてください。
|
||||
6. ユーザーの疑問や、ある概念への不確かさがある場合は、ツールを使って情報収集や意味調査をして構いません。複数ツールを使ってもよいです。
|
||||
|
||||
|
||||
@@ -8,11 +8,10 @@
|
||||
在当前场景中,不同的人正在互动({bot_name} 也是一位参与的用户),用户也可能正在连续发送消息或彼此互动。
|
||||
你的任务不是生成对别人可见的发言,也不是直接使用查询类工具,而是判断当前是否应该:
|
||||
- continue:立刻进入下一轮完整思考、搜集信息、回复与其他工具执行
|
||||
- wait:固定再等待一段时间,时间到后再重新判断;
|
||||
- no_reply:本轮不继续,直接等待新的消息
|
||||
- no_reply:本轮不继续发言,等待新的消息;也用于用户可能还没说完、需要先把发言权交还给用户的场景
|
||||
|
||||
节奏控制规则:
|
||||
1. 如果 {bot_name} 已经回复,但用户暂时没有新的回复,且没有新信息需要搜集,使用 wait 或者 no_reply 进行等待。
|
||||
1. 如果 {bot_name} 已经回复,但用户暂时没有新的回复,且没有新信息需要搜集,使用 no_reply 进行等待。
|
||||
2. 如果用户有新发言,但是你评估用户还有后续发言尚未发送,可以适当等待让用户说完。
|
||||
3. 你需要先评估是用户之间在互动还是和{bot_name}在互动,不要盲目插话,弄错回复对象
|
||||
4. 你需要评估哪些话是对{bot_name}的发言,哪些是用户之间的交流或者自言自语,根据情况适当发言。
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytest
|
||||
|
||||
from src.core.tooling import ToolExecutionResult, ToolInvocation
|
||||
from src.llm_models.payload_content.tool_option import ToolCall
|
||||
from src.maisaka.builtin_tool import get_timing_tools
|
||||
from src.maisaka.chat_loop_service import ChatResponse, MaisakaChatLoopService
|
||||
from src.maisaka.context_messages import AssistantMessage, TIMING_GATE_INVALID_TOOL_HINT_SOURCE
|
||||
from src.maisaka.reasoning_engine import MaisakaReasoningEngine
|
||||
@@ -32,6 +33,12 @@ def _build_chat_response(tool_calls: list[ToolCall]) -> ChatResponse:
|
||||
)
|
||||
|
||||
|
||||
def test_timing_gate_tools_only_expose_continue_and_no_reply() -> None:
|
||||
tool_names = {tool_definition["name"] for tool_definition in get_timing_tools()}
|
||||
|
||||
assert tool_names == {"continue", "no_reply"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timing_gate_invalid_tool_defaults_to_no_reply(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
runtime = SimpleNamespace(
|
||||
|
||||
@@ -3,12 +3,13 @@ from __future__ import annotations
|
||||
from argparse import ArgumentParser, Namespace
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import DefaultDict
|
||||
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
@@ -45,6 +46,35 @@ def parse_datetime_filter(value: str | None) -> str | None:
|
||||
raise ValueError(f"无法解析时间: {value!r},请使用 YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS")
|
||||
|
||||
|
||||
def parse_recent_filter(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
normalized_value = value.strip().lower()
|
||||
if not normalized_value:
|
||||
return None
|
||||
|
||||
match = re.fullmatch(r"(\d+(?:\.\d+)?)([mhdw])", normalized_value)
|
||||
if match is None:
|
||||
raise ValueError(f"无法解析最近时间: {value!r},请使用 30m、24h、7d 或 2w")
|
||||
|
||||
amount = float(match.group(1))
|
||||
unit = match.group(2)
|
||||
if amount <= 0:
|
||||
raise ValueError(f"最近时间必须大于 0: {value!r}")
|
||||
|
||||
if unit == "m":
|
||||
delta = timedelta(minutes=amount)
|
||||
elif unit == "h":
|
||||
delta = timedelta(hours=amount)
|
||||
elif unit == "d":
|
||||
delta = timedelta(days=amount)
|
||||
else:
|
||||
delta = timedelta(weeks=amount)
|
||||
|
||||
return (datetime.now() - delta).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def connect_readonly(db_path: Path) -> sqlite3.Connection:
|
||||
if not db_path.exists():
|
||||
raise FileNotFoundError(f"数据库文件不存在: {db_path}")
|
||||
@@ -249,6 +279,7 @@ def print_csv(rows: list[ToolUsageRow]) -> None:
|
||||
def parse_args() -> Namespace:
|
||||
parser = ArgumentParser(description="统计不同 chat_id 的工具使用次数和占比。")
|
||||
parser.add_argument("--db", type=Path, default=DEFAULT_DB_PATH, help=f"数据库路径,默认: {DEFAULT_DB_PATH}")
|
||||
parser.add_argument("--recent", help="统计最近多久的记录,例如: 30m、24h、7d、2w;如果同时指定 --since,则优先使用 --since")
|
||||
parser.add_argument("--since", help="仅统计此时间之后的记录,格式: YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS")
|
||||
parser.add_argument("--until", help="仅统计此时间之前的记录,格式: YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS")
|
||||
parser.add_argument("--min-chat-total", type=int, default=1, help="只显示工具调用总数不低于该值的 chat_id")
|
||||
@@ -261,7 +292,7 @@ def parse_args() -> Namespace:
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
since = parse_datetime_filter(args.since)
|
||||
since = parse_datetime_filter(args.since) or parse_recent_filter(args.recent)
|
||||
until = parse_datetime_filter(args.until)
|
||||
min_chat_total = max(1, int(args.min_chat_total))
|
||||
top_tools = args.top_tools if args.top_tools is None else max(1, int(args.top_tools))
|
||||
|
||||
@@ -435,7 +435,7 @@ class ChatConfig(ConfigBase):
|
||||
"advanced": True,
|
||||
},
|
||||
)
|
||||
"""Timing Gate 返回 wait/no_reply 时的最小窗口秒数,0 表示不启用冷却"""
|
||||
"""Timing Gate 返回 no_reply 时的最小窗口秒数,0 表示不启用冷却"""
|
||||
|
||||
group_chat_prompt: str = Field(
|
||||
default=(
|
||||
|
||||
@@ -30,8 +30,6 @@ from .tool_search import get_tool_spec as get_tool_search_tool_spec
|
||||
from .tool_search import handle_tool as handle_tool_search_tool
|
||||
from .view_complex_message import get_tool_spec as get_view_complex_message_tool_spec
|
||||
from .view_complex_message import handle_tool as handle_view_complex_message_tool
|
||||
from .wait import get_tool_spec as get_wait_tool_spec
|
||||
from .wait import handle_tool as handle_wait_tool
|
||||
|
||||
BuiltinToolHandler = Callable[[ToolInvocation, Optional[ToolExecutionContext]], Awaitable[ToolExecutionResult]]
|
||||
BuiltinToolRawHandler = Callable[
|
||||
@@ -70,7 +68,6 @@ def _get_query_memory_tool_spec() -> ToolSpec:
|
||||
|
||||
|
||||
BUILTIN_TOOL_ENTRIES: List[BuiltinToolEntry] = [
|
||||
BuiltinToolEntry("wait", get_wait_tool_spec, handle_wait_tool, stage="timing"),
|
||||
BuiltinToolEntry("no_reply", get_no_reply_tool_spec, handle_no_reply_tool, stage="timing"),
|
||||
BuiltinToolEntry("continue", get_continue_tool_spec, handle_continue_tool, stage="timing"),
|
||||
BuiltinToolEntry("finish", get_finish_tool_spec, handle_finish_tool, stage="action"),
|
||||
|
||||
@@ -12,7 +12,7 @@ def get_tool_spec() -> ToolSpec:
|
||||
|
||||
return ToolSpec(
|
||||
name="no_reply",
|
||||
brief_description="本轮不进行回复,等待其他用户的新消息。",
|
||||
brief_description="本轮不进行回复,等待其他用户的新消息;也用于用户可能还没说完、需要先把发言权交还给用户的场景。",
|
||||
provider_name="maisaka_builtin",
|
||||
provider_type="builtin",
|
||||
)
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
"""wait 内置工具。"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from src.core.tooling import ToolExecutionContext, ToolExecutionResult, ToolInvocation, ToolSpec
|
||||
|
||||
from .context import BuiltinToolRuntimeContext
|
||||
|
||||
|
||||
def get_tool_spec() -> ToolSpec:
|
||||
"""获取 wait 工具声明。"""
|
||||
|
||||
return ToolSpec(
|
||||
name="wait",
|
||||
brief_description="暂停当前对话并固定等待一段时间,期间不因新消息提前恢复。",
|
||||
detailed_description="参数说明:\n- seconds:integer,必填。等待的秒数。等待期间收到的新消息只会暂存,直到超时后再继续处理。",
|
||||
parameters_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"seconds": {
|
||||
"type": "integer",
|
||||
"description": "等待的秒数。",
|
||||
},
|
||||
},
|
||||
"required": ["seconds"],
|
||||
},
|
||||
provider_name="maisaka_builtin",
|
||||
provider_type="builtin",
|
||||
)
|
||||
|
||||
|
||||
async def handle_tool(
|
||||
tool_ctx: BuiltinToolRuntimeContext,
|
||||
invocation: ToolInvocation,
|
||||
context: Optional[ToolExecutionContext] = None,
|
||||
) -> ToolExecutionResult:
|
||||
"""执行 wait 内置工具。"""
|
||||
|
||||
del context
|
||||
seconds = invocation.arguments.get("seconds", 30)
|
||||
try:
|
||||
wait_seconds = int(seconds)
|
||||
except (TypeError, ValueError):
|
||||
wait_seconds = 30
|
||||
wait_seconds = max(0, wait_seconds)
|
||||
tool_ctx.runtime._enter_wait_state(seconds=wait_seconds, tool_call_id=invocation.call_id)
|
||||
return tool_ctx.build_success_result(
|
||||
invocation.tool_name,
|
||||
f"当前对话循环进入等待状态,将固定等待 {wait_seconds} 秒;期间收到的新消息不会提前打断本次等待。",
|
||||
metadata={"pause_execution": True},
|
||||
)
|
||||
@@ -40,7 +40,7 @@ from .history_utils import drop_orphan_tool_results, normalize_tool_result_order
|
||||
from .display.prompt_cli_renderer import PromptCLIVisualizer
|
||||
from .visual_mode_utils import resolve_enable_visual_planner
|
||||
|
||||
TIMING_GATE_TOOL_NAMES = {"continue", "no_reply", "wait"}
|
||||
TIMING_GATE_TOOL_NAMES = {"continue", "no_reply"}
|
||||
REQUEST_TYPE_BY_REQUEST_KIND = {
|
||||
"planner": "maisaka_planner",
|
||||
"timing_gate": "maisaka_timing_gate",
|
||||
|
||||
@@ -54,7 +54,7 @@ logger = get_logger("maisaka_reasoning_engine")
|
||||
|
||||
TIMING_GATE_CONTEXT_DROP_HEAD_RATIO = 0.7
|
||||
TIMING_GATE_MAX_ATTEMPTS = 3
|
||||
TIMING_GATE_TOOL_NAMES = {"continue", "no_reply", "wait"}
|
||||
TIMING_GATE_TOOL_NAMES = {"continue", "no_reply"}
|
||||
HISTORY_SILENT_TOOL_NAMES = {"finish"}
|
||||
|
||||
|
||||
@@ -149,10 +149,9 @@ class MaisakaReasoningEngine:
|
||||
return (
|
||||
"你是 Maisaka 的 timing gate 子代理,只负责决定当前会话下一步的节奏控制。\n"
|
||||
"你必须且只能调用一个工具,不要输出普通文本答案。\n"
|
||||
"可用工具只有三个:\n"
|
||||
"1. wait: 适合暂时等待一段时间,再重新判断是否继续。\n"
|
||||
"2. no_reply: 适合当前不继续本轮,直接等待新的外部消息。\n"
|
||||
"3. continue: 适合现在立刻进入下一轮正常思考、回复、查询和其他工具执行。\n"
|
||||
"可用工具只有两个:\n"
|
||||
"1. no_reply: 适合当前不继续本轮发言,等待新的外部消息或让用户继续说完。\n"
|
||||
"2. continue: 适合现在立刻进入下一轮正常思考、回复、查询和其他工具执行。\n"
|
||||
"如果需要真正回复消息、查询信息或使用其他工具,应该调用 continue,让主分支继续执行,而不是在这里完成。\n"
|
||||
"不要连续调用多个工具,也不要输出工具之外的计划。"
|
||||
)
|
||||
@@ -242,7 +241,7 @@ class MaisakaReasoningEngine:
|
||||
async def _run_timing_gate(
|
||||
self,
|
||||
anchor_message: SessionMessage,
|
||||
) -> tuple[Literal["continue", "no_reply", "wait"], Any, list[str], list[dict[str, Any]]]:
|
||||
) -> tuple[Literal["continue", "no_reply"], Any, list[str], list[dict[str, Any]]]:
|
||||
"""运行 Timing Gate 子代理并返回控制决策。"""
|
||||
|
||||
if self._runtime._force_next_timing_continue:
|
||||
@@ -388,7 +387,7 @@ class MaisakaReasoningEngine:
|
||||
hint_content = (
|
||||
"Timing Gate 上一轮选择了非法工具:"
|
||||
f"{normalized_tool_text}。\n"
|
||||
"Timing Gate 只能调用 continue、wait 或 no_reply 中的一个工具。"
|
||||
"Timing Gate 只能调用 continue 或 no_reply 中的一个工具。"
|
||||
)
|
||||
self._runtime._chat_history.append(
|
||||
SessionBackedMessage(
|
||||
|
||||
@@ -572,7 +572,7 @@ class MaisakaHeartFlowChatting:
|
||||
return self._force_next_timing_continue
|
||||
|
||||
async def _wait_for_timing_gate_non_continue_cooldown(self, elapsed_seconds: float) -> None:
|
||||
"""仅对 Timing Gate 的 wait/no_reply 动作应用冷却窗口。"""
|
||||
"""仅对 Timing Gate 的 no_reply 动作应用冷却窗口。"""
|
||||
|
||||
cooldown_seconds = self._timing_gate_non_continue_cooldown_seconds
|
||||
if cooldown_seconds <= 0:
|
||||
|
||||
@@ -44,7 +44,6 @@ BUILTIN_TOOL_NAMES = frozenset(
|
||||
{
|
||||
"reply",
|
||||
"no_reply",
|
||||
"wait",
|
||||
"stop",
|
||||
"create_table",
|
||||
"list_tables",
|
||||
|
||||
Reference in New Issue
Block a user