feat(core): 新增 Phase 10 ContextSlot 多上下文分区管理
- 新增 ContextSlot 类型(Full / Focused / Readonly 三种模式, New / Derived / Static 三种来源),支持 JSON blob 批次持久化 - AgentSession 新增 slots 字段与 5 个管理方法 (create_slot / switch_slot / list_slots / derive_slot / delete_slot), 自动创建 "default" slot - submit_turn / finalize_turn 改造为基于当前 slot 的增量追加写回, 确保 Focused 模式"读时过滤"语义不丢失数据 - finalize_turn 签名变更(新增 new_messages_from_cycle 参数, 返回 Result<(), AgentError>),向后兼容列于 docs/17 - 新增 3 个 AgentError 变体(SlotReadonly / SlotNotFound / SlotAlreadyExists) - 新增分支对话示例 context_slot_demo(法律咨询→两个派生方向→切换→隔离验证) - 新增 43 个测试覆盖持久化、Focused 过滤、Readonly 阻断、delete 保护、 派生逻辑、流式 finalize_turn、key 注入防护等场景 - 方案文档:docs/17-phase10-contextslot.md(含 §5 推荐方案、§6 实施建议、 §9 实施计划,经过 4 轮方案/计划/实施审查 + 1 轮非阻塞建议修复)
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
//! context_slot_demo —— 多上下文槽位管理示例。
|
||||
//!
|
||||
//! 场景:法律咨询入口 → 派生两个独立探索方向 → 切换 → 隔离验证 → 删除。
|
||||
//!
|
||||
//! 展示:
|
||||
//! - 默认 slot 自动创建
|
||||
//! - 多 slot 间的消息隔离
|
||||
//! - 派生 slot 从父 slot 复制消息
|
||||
//! - 删除非 default slot 后自动回退到 default
|
||||
//!
|
||||
//! 运行:`cargo run --example context_slot_demo`(离线,零配置)
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agcore::agent::{Agent, AgentBuilder, AgentSession};
|
||||
use agcore::llm::hooks::HookExecutor;
|
||||
use agcore::llm::mock::MockProvider;
|
||||
use agcore::llm::provider::LlmProvider;
|
||||
use agcore::llm::types::message::{ContentBlock, Message};
|
||||
use agcore::llm::types::response_v2::{MessageResponse, StopReason};
|
||||
use agcore::llm::types::Usage;
|
||||
use agcore::tools::ToolRegistry;
|
||||
|
||||
struct LegalAdvisor;
|
||||
|
||||
impl Agent for LegalAdvisor {
|
||||
fn name(&self) -> &str {
|
||||
"legal-advisor"
|
||||
}
|
||||
fn system_prompt(&self) -> Option<&str> {
|
||||
Some("你是法律顾问。请用一句话回答用户问题。")
|
||||
}
|
||||
}
|
||||
|
||||
/// 构造一个简单的 Assistant 响应(用于 MockProvider)。
|
||||
fn assistant_resp(text: &str) -> MessageResponse {
|
||||
MessageResponse {
|
||||
id: String::new(),
|
||||
model: "mock".into(),
|
||||
message: Message::Assistant {
|
||||
content: vec![ContentBlock::Text { text: text.into() }],
|
||||
},
|
||||
usage: Usage::from_input_output(5, 5),
|
||||
stop_reason: StopReason::Stop,
|
||||
extra: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// 1. 构造 session(自动包含 default slot)
|
||||
let provider: Arc<dyn LlmProvider> = Arc::new(MockProvider::new(vec![
|
||||
assistant_resp("您好,我可以帮您处理法律问题。"),
|
||||
assistant_resp("管辖权问题:建议选择合同签订地法院。"),
|
||||
assistant_resp("条款修改:建议将上限调整为 80 万。"),
|
||||
assistant_resp("已回到主对话。"),
|
||||
]));
|
||||
let bundle = Arc::new(
|
||||
AgentBuilder::new()
|
||||
.provider(provider)
|
||||
.tool_registry(Arc::new(ToolRegistry::new()))
|
||||
.hook_executor(Arc::new(HookExecutor::new()))
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
let mut session = AgentSession::new(Arc::new(LegalAdvisor), "legal-001", bundle);
|
||||
|
||||
println!("=== 1. 默认 slot 自动创建 ===");
|
||||
assert_eq!(session.current_slot_id(), "default");
|
||||
let slots: Vec<_> = session.list_slots().collect();
|
||||
println!("初始 slots: {slots:?}");
|
||||
assert_eq!(slots.len(), 1);
|
||||
assert!(slots.contains(&&"default".to_string()));
|
||||
|
||||
println!("\n=== 2. 在 default slot 中提交一轮 ===");
|
||||
let r1 = session.submit_turn("我需要法律援助").await.unwrap();
|
||||
println!("default slot response: {}", r1.text());
|
||||
|
||||
println!("\n=== 3. 派生两个独立探索方向的 slot ===");
|
||||
session
|
||||
.derive_slot("option_jurisdiction", "default", agcore::agent::DeriveStrategy::Full)
|
||||
.await
|
||||
.unwrap();
|
||||
session
|
||||
.derive_slot("option_amendment", "default", agcore::agent::DeriveStrategy::Full)
|
||||
.await
|
||||
.unwrap();
|
||||
let slots: Vec<_> = session.list_slots().cloned().collect();
|
||||
println!("派生后 slots: {slots:?}");
|
||||
assert_eq!(slots.len(), 3);
|
||||
|
||||
println!("\n=== 4. 切到 option_jurisdiction 并提交 ===");
|
||||
session.switch_slot("option_jurisdiction").await.unwrap();
|
||||
assert_eq!(session.current_slot_id(), "option_jurisdiction");
|
||||
let r2 = session.submit_turn("如果用户质疑管辖权?").await.unwrap();
|
||||
println!("option_jurisdiction response: {}", r2.text());
|
||||
|
||||
println!("\n=== 5. 切到 option_amendment 并提交 ===");
|
||||
session.switch_slot("option_amendment").await.unwrap();
|
||||
let r3 = session.submit_turn("用户要求提高赔偿上限?").await.unwrap();
|
||||
println!("option_amendment response: {}", r3.text());
|
||||
|
||||
println!("\n=== 6. 切回 default,验证消息隔离 ===");
|
||||
session.switch_slot("default").await.unwrap();
|
||||
let r4 = session.submit_turn("汇总一下我们的讨论").await.unwrap();
|
||||
println!("default response: {}", r4.text());
|
||||
// 验证 default slot 不包含 option_jurisdiction 的"管辖权"问题
|
||||
let (_, default_slot) = session.slots().find(|(id, _)| *id == "default").unwrap();
|
||||
let default_has_jurisdiction = default_slot
|
||||
.messages
|
||||
.iter()
|
||||
.any(|m| message_contains(m, "管辖权"));
|
||||
assert!(
|
||||
!default_has_jurisdiction,
|
||||
"default slot 不应包含 option_jurisdiction 的消息"
|
||||
);
|
||||
|
||||
println!("\n=== 7. 删除 option_amendment,验证回退到 default ===");
|
||||
session.delete_slot("option_amendment").await.unwrap();
|
||||
let slots: Vec<_> = session.list_slots().cloned().collect();
|
||||
println!("删除后 slots: {slots:?}");
|
||||
assert!(!slots.contains(&"option_amendment".to_string()));
|
||||
assert_eq!(slots.len(), 2);
|
||||
|
||||
println!("\n=== 8. 切到 option_jurisdiction 并删除,验证 current 回退 ===");
|
||||
session.switch_slot("option_jurisdiction").await.unwrap();
|
||||
session.delete_slot("option_jurisdiction").await.unwrap();
|
||||
assert_eq!(session.current_slot_id(), "default");
|
||||
let slots: Vec<_> = session.list_slots().cloned().collect();
|
||||
println!("删除后 slots: {slots:?}");
|
||||
assert_eq!(slots.len(), 1);
|
||||
assert_eq!(slots[0], "default");
|
||||
|
||||
println!("\n=== 9. 验证 delete_slot 保护逻辑 ===");
|
||||
let err = session.delete_slot("default").await.unwrap_err();
|
||||
println!("删除 default 返回错误: {err}");
|
||||
assert!(matches!(err, agcore::agent::AgentError::Config(_)));
|
||||
|
||||
println!("\n✓ context_slot_demo 完成");
|
||||
}
|
||||
|
||||
/// 检查 Message 是否包含指定文本(提取第一个 Text block)。
|
||||
fn message_contains(msg: &Message, needle: &str) -> bool {
|
||||
use agcore::llm::types::message::ContentBlock;
|
||||
let blocks = match msg {
|
||||
Message::System { content }
|
||||
| Message::User { content }
|
||||
| Message::Assistant { content } => content,
|
||||
Message::UserImage { .. } => return false,
|
||||
Message::ToolResult { content, .. } => content,
|
||||
_ => return false,
|
||||
};
|
||||
for block in blocks {
|
||||
if let ContentBlock::Text { text } = block
|
||||
&& text.contains(needle)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
Reference in New Issue
Block a user