feat(scheduler): 添加上下文支持到调度器管理工具,增强目标处理能力

This commit is contained in:
ooodc 2026-04-23 23:26:41 +08:00
parent 60749671e9
commit e24a081293
3 changed files with 97 additions and 6 deletions

View File

@ -37,11 +37,17 @@
## 写入规则 ## 写入规则
- 写入或修改记忆时,再使用 memory_manage。 - 写入或修改记忆时,再使用 memory_manage。
- 仅在遇到高价值且未来仍有用的信息时写入记忆:用户长期偏好、稳定事实、用户对你的纠正、持续任务或项目上下文、明确决策。 - 仅在遇到高价值且未来仍有用的信息时写入记忆:用户长期偏好、稳定事实、用户对你的纠正、持续任务或项目上下文、明确决策
- 不要保存一次性工具结果、临时列表、敏感凭证或不确定推测。 - 不要保存一次性工具结果、临时列表、敏感凭证或不确定推测。
- 写入时优先使用规范 namespacepreferences、profile、tasks、decisions。 - 写入时优先使用规范 namespacepreferences、profile、tasks、decisions。
- 优先调用 memory_manage(action='put');同一 namespace/key 可直接覆盖更新。 - 优先调用 memory_manage(action='put');同一 namespace/key 可直接覆盖更新。
### 以下场景视为高价值加分
- 用户多次交互优化输出
- 用户对你的纠正
- 确定的事实,路径/地址/网址等
- 用户独特的表达,缩写/非常规的表达
## 最后检查 ## 最后检查
如果你决定跳过记忆搜索,应先确认当前请求确实属于上述少数例外,而不是因为你忘了检索,或因为你误以为单凭当前消息就足够。 如果你决定跳过记忆搜索,应先确认当前请求确实属于上述少数例外,而不是因为你忘了检索,或因为你误以为单凭当前消息就足够。

View File

@ -20,6 +20,7 @@
- 对不确定的地方要直说,不把猜测包装成事实。 - 对不确定的地方要直说,不把猜测包装成事实。
- 复杂任务先收敛重点,简单任务直接给结果。 - 复杂任务先收敛重点,简单任务直接给结果。
- 避免不必要的重复、客套和冗长说明。 - 避免不必要的重复、客套和冗长说明。
- 调用工具的时候需要不仅仅回复工具的json最好也简短说明你调用工具要完成什么工作
## 回复规则 ## 回复规则
@ -30,6 +31,4 @@
## 补充要求 ## 补充要求
- 你是 PicoBot。
- 回答应以帮助用户完成当前目标为中心。
- 在信息不足时先补关键前提,在信息充分时直接执行。 - 在信息不足时先补关键前提,在信息充分时直接执行。

View File

@ -74,6 +74,15 @@ impl Tool for SchedulerManageTool {
} }
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> { async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
self.execute_with_context(&crate::tools::ToolContext::default(), args)
.await
}
async fn execute_with_context(
&self,
context: &crate::tools::ToolContext,
args: serde_json::Value,
) -> anyhow::Result<ToolResult> {
let action = match args.get("action").and_then(|value| value.as_str()) { let action = match args.get("action").and_then(|value| value.as_str()) {
Some(action) => action, Some(action) => action,
None => return Ok(error_result("Missing required parameter: action")), None => return Ok(error_result("Missing required parameter: action")),
@ -96,7 +105,7 @@ impl Tool for SchedulerManageTool {
} }
} }
"put" => { "put" => {
let input = build_upsert(&args, &self.known_agents)?; let input = build_upsert(context, &args, &self.known_agents)?;
let record = self.store.upsert_scheduler_job(&input)?; let record = self.store.upsert_scheduler_job(&input)?;
record_to_json(&record) record_to_json(&record)
} }
@ -145,7 +154,11 @@ impl Tool for SchedulerManageTool {
} }
} }
fn build_upsert(args: &serde_json::Value, known_agents: &HashSet<String>) -> anyhow::Result<SchedulerJobUpsert> { fn build_upsert(
context: &crate::tools::ToolContext,
args: &serde_json::Value,
known_agents: &HashSet<String>,
) -> anyhow::Result<SchedulerJobUpsert> {
let id = require_str(args, "id")?.to_string(); let id = require_str(args, "id")?.to_string();
let kind = require_str(args, "kind")?.to_string(); let kind = require_str(args, "kind")?.to_string();
let schedule_value = args let schedule_value = args
@ -164,7 +177,10 @@ fn build_upsert(args: &serde_json::Value, known_agents: &HashSet<String>) -> any
}; };
let payload = args.get("payload").cloned().unwrap_or_else(|| json!({})); let payload = args.get("payload").cloned().unwrap_or_else(|| json!({}));
let target = args.get("target").cloned().unwrap_or_else(|| json!({})); let target = enrich_target_from_context(
args.get("target").cloned().unwrap_or_else(|| json!({})),
context,
);
if kind == "agent_task" { if kind == "agent_task" {
validate_agent_task_payload(&payload, known_agents)?; validate_agent_task_payload(&payload, known_agents)?;
validate_target_fields(&target, &["channel", "chat_id"], "agent_task")?; validate_target_fields(&target, &["channel", "chat_id"], "agent_task")?;
@ -198,6 +214,38 @@ fn build_upsert(args: &serde_json::Value, known_agents: &HashSet<String>) -> any
}) })
} }
fn enrich_target_from_context(
target: serde_json::Value,
context: &crate::tools::ToolContext,
) -> serde_json::Value {
let mut object = match target {
serde_json::Value::Object(map) => map,
_ => return target,
};
if !has_non_empty_string(&object, "channel") {
if let Some(channel_name) = context.channel_name.as_ref().filter(|value| !value.trim().is_empty()) {
object.insert("channel".to_string(), serde_json::Value::String(channel_name.clone()));
}
}
if !has_non_empty_string(&object, "chat_id") {
if let Some(chat_id) = context.chat_id.as_ref().filter(|value| !value.trim().is_empty()) {
object.insert("chat_id".to_string(), serde_json::Value::String(chat_id.clone()));
}
}
serde_json::Value::Object(object)
}
fn has_non_empty_string(object: &serde_json::Map<String, serde_json::Value>, field: &str) -> bool {
object
.get(field)
.and_then(|value| value.as_str())
.map(|value| !value.trim().is_empty())
.unwrap_or(false)
}
fn validate_agent_task_payload(payload: &serde_json::Value, known_agents: &HashSet<String>) -> anyhow::Result<()> { fn validate_agent_task_payload(payload: &serde_json::Value, known_agents: &HashSet<String>) -> anyhow::Result<()> {
let Some(prompt) = payload.get("prompt").and_then(|value| value.as_str()) else { let Some(prompt) = payload.get("prompt").and_then(|value| value.as_str()) else {
anyhow::bail!("agent_task payload.prompt is required and must be a string") anyhow::bail!("agent_task payload.prompt is required and must be a string")
@ -411,6 +459,44 @@ mod tests {
assert!(error.contains("outbound_message target.channel is required")); assert!(error.contains("outbound_message target.channel is required"));
} }
#[tokio::test]
async fn test_scheduler_manage_put_uses_tool_context_target_defaults() {
let store = Arc::new(SessionStore::in_memory().unwrap());
let tool = SchedulerManageTool::new(store.clone(), HashSet::new());
let put_result = tool
.execute_with_context(
&crate::tools::ToolContext {
channel_name: Some("feishu".to_string()),
sender_id: Some("user-1".to_string()),
chat_id: Some("oc_demo".to_string()),
session_id: Some("feishu:oc_demo".to_string()),
message_id: Some("msg-1".to_string()),
message_seq: Some(1),
},
json!({
"action": "put",
"id": "work_reminder",
"kind": "outbound_message",
"schedule": {
"type": "interval",
"seconds": 60
},
"payload": {
"content": "ping"
}
}),
)
.await
.unwrap();
assert!(put_result.success);
let saved = store.get_scheduler_job("work_reminder").unwrap().unwrap();
assert_eq!(saved.target["channel"], "feishu");
assert_eq!(saved.target["chat_id"], "oc_demo");
}
#[tokio::test] #[tokio::test]
async fn test_scheduler_manage_rejects_unknown_agent_task_agent() { async fn test_scheduler_manage_rejects_unknown_agent_task_agent() {
let store = Arc::new(SessionStore::in_memory().unwrap()); let store = Arc::new(SessionStore::in_memory().unwrap());