feat(examples): 新增 Phase B 7 个离线可运行示例
新增 7 个示例覆盖全 Phase 公共 API(不依赖 API key 即可运行): - agent_session_demo:AgentBuilder → AgentSession → SessionMemory - custom_tool:BaseTool 注册 + invoke/invoke_all + 权限检查 - prompt_composer:PromptTemplate + PromptComposer + validate_messages - task_agent_demo:JsonPlanParser + Step 状态机 + 错误路径 - conversation_memory_demo:滑动窗口 + 多角色 + 隔离 - knowledge_search_demo:KnowledgeStore + 关键词检索 + 停用词过滤 - streaming_events_demo:submit_stream 事件消费 + 队列耗尽错误路径
This commit is contained in:
@@ -0,0 +1,130 @@
|
|||||||
|
//! agent_session_demo —— Agent 装配 + 会话链路 + SessionMemory 桥接。
|
||||||
|
//!
|
||||||
|
//! 演示:
|
||||||
|
//! 1. 实现 `Agent` trait(定义角色 + system prompt)
|
||||||
|
//! 2. 用 `MockProvider` 预设响应(离线可跑)
|
||||||
|
//! 3. `AgentBuilder` 装配 `RuntimeBundle`
|
||||||
|
//! 4. `AgentSession::submit_turn` 跑多轮对话
|
||||||
|
//! 5. `SessionMemory` 读写 + snapshot 输出
|
||||||
|
//! 6. 跨 session 数据隔离验证
|
||||||
|
//!
|
||||||
|
//! 运行:`cargo run --example agent_session_demo`
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use agcore::agent::{Agent, AgentBuilder, AgentSession};
|
||||||
|
use agcore::llm::hooks::HookExecutor;
|
||||||
|
use agcore::llm::mock::MockProvider;
|
||||||
|
use agcore::llm::types::message::{ContentBlock, Message};
|
||||||
|
use agcore::llm::types::response_v2::{MessageResponse, StopReason};
|
||||||
|
use agcore::llm::types::Usage;
|
||||||
|
use agcore::tools::ToolRegistry;
|
||||||
|
|
||||||
|
/// 计算器角色 Agent。
|
||||||
|
struct CalculatorAgent;
|
||||||
|
|
||||||
|
impl Agent for CalculatorAgent {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"calculator"
|
||||||
|
}
|
||||||
|
fn system_prompt(&self) -> Option<&str> {
|
||||||
|
Some("你是一个简洁的计算器助手,每轮回答一句话。")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构造预设的纯文本 Assistant 响应。
|
||||||
|
fn assistant_text(text: &str) -> MessageResponse {
|
||||||
|
MessageResponse {
|
||||||
|
id: String::new(),
|
||||||
|
model: String::new(),
|
||||||
|
message: Message::Assistant {
|
||||||
|
content: vec![ContentBlock::Text { text: text.into() }],
|
||||||
|
},
|
||||||
|
usage: Usage::from_input_output(8, 4),
|
||||||
|
stop_reason: StopReason::Stop,
|
||||||
|
extra: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// 1. MockProvider:预设三轮响应(无须 API key 即可离线运行)
|
||||||
|
let provider = Arc::new(MockProvider::new(vec![
|
||||||
|
assistant_text("1 + 1 = 2"),
|
||||||
|
assistant_text("2 + 2 = 4"),
|
||||||
|
assistant_text("会话即将结束。"),
|
||||||
|
]));
|
||||||
|
|
||||||
|
// 2. AgentBuilder 装配 RuntimeBundle(必填:provider / tool_registry / hook_executor)
|
||||||
|
let bundle = Arc::new(
|
||||||
|
AgentBuilder::new()
|
||||||
|
.provider(provider)
|
||||||
|
.tool_registry(Arc::new(ToolRegistry::new()))
|
||||||
|
.hook_executor(Arc::new(HookExecutor::new()))
|
||||||
|
.build()
|
||||||
|
.expect("RuntimeBundle 装配失败"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. 创建会话
|
||||||
|
let agent: Arc<dyn Agent> = Arc::new(CalculatorAgent);
|
||||||
|
let mut session = AgentSession::new(agent, "demo-session", bundle.clone());
|
||||||
|
assert_eq!(session.turn_index(), 0);
|
||||||
|
|
||||||
|
// 4. 提交第一轮
|
||||||
|
println!("=== 提交第 1 轮 ===");
|
||||||
|
let resp = session.submit_turn("1+1=?").await.expect("submit_turn 失败");
|
||||||
|
println!("LLM: {}", resp.text());
|
||||||
|
session
|
||||||
|
.set_session_data("last_q", "1+1=?")
|
||||||
|
.await
|
||||||
|
.expect("set_session_data 失败");
|
||||||
|
session
|
||||||
|
.set_session_data("last_a", resp.text())
|
||||||
|
.await
|
||||||
|
.expect("set_session_data 失败");
|
||||||
|
assert_eq!(session.turn_index(), 1);
|
||||||
|
|
||||||
|
// 5. 提交第二轮
|
||||||
|
println!("\n=== 提交第 2 轮 ===");
|
||||||
|
let resp = session.submit_turn("再加一次 2+2=?").await.unwrap();
|
||||||
|
println!("LLM: {}", resp.text());
|
||||||
|
assert_eq!(session.turn_index(), 2);
|
||||||
|
|
||||||
|
// 6. 验证 SessionMemory 读取
|
||||||
|
println!("\n=== Session Memory 读取 ===");
|
||||||
|
println!(
|
||||||
|
"last_q = {:?}",
|
||||||
|
session.get_session_data("last_q").await.unwrap()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"last_a = {:?}",
|
||||||
|
session.get_session_data("last_a").await.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 7. Snapshot 格式化输出
|
||||||
|
println!("\n=== Session Memory Snapshot ===");
|
||||||
|
println!("{}", session.session_memory().snapshot().await.unwrap());
|
||||||
|
|
||||||
|
// 8. 跨 session 数据隔离验证
|
||||||
|
println!("=== 数据隔离验证 ===");
|
||||||
|
let other = AgentSession::new(
|
||||||
|
Arc::new(CalculatorAgent),
|
||||||
|
"other-session",
|
||||||
|
bundle,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
other.get_session_data("last_q").await.unwrap().is_none(),
|
||||||
|
"新会话不应看到旧 session 的 last_q"
|
||||||
|
);
|
||||||
|
println!("新会话 last_q = None ✓");
|
||||||
|
|
||||||
|
// 9. 用量累计验证
|
||||||
|
println!("\n=== 用量累计 ===");
|
||||||
|
let total = session.usage().total();
|
||||||
|
println!(
|
||||||
|
"prompt={}, completion={}, total={}",
|
||||||
|
total.prompt_tokens, total.completion_tokens, total.total_tokens
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("\n✓ agent_session_demo 完成");
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
//! conversation_memory_demo —— 对话记忆滑动窗口与隔离。
|
||||||
|
//!
|
||||||
|
//! 演示:
|
||||||
|
//! 1. `ConversationMemoryConfig` 构造(SlidingWindow / Full 策略)
|
||||||
|
//! 2. `add_message` 写入多角色消息(Message IR)
|
||||||
|
//! 3. 滑动窗口自动淘汰旧消息
|
||||||
|
//! 4. Full 策略保留全部
|
||||||
|
//! 5. `get_history` / `len` / `clear`
|
||||||
|
//! 6. 跨 session 数据隔离(共用 MemoryStore)
|
||||||
|
//!
|
||||||
|
//! 运行:`cargo run --example conversation_memory_demo`
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use agcore::llm::types::message::Message;
|
||||||
|
use agcore::memory::{ConversationMemory, ConversationMemoryConfig, InMemoryStore, MemoryStrategy};
|
||||||
|
|
||||||
|
fn message_text(msg: &Message) -> &str {
|
||||||
|
match msg {
|
||||||
|
Message::User { content }
|
||||||
|
| Message::System { content }
|
||||||
|
| Message::Assistant { content }
|
||||||
|
| Message::ToolResult { content, .. } => content
|
||||||
|
.iter()
|
||||||
|
.filter_map(|b| match b {
|
||||||
|
agcore::llm::types::message::ContentBlock::Text { text } => Some(text.as_str()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.unwrap_or(""),
|
||||||
|
Message::UserImage { .. } => "[image]",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// 1. 滑动窗口策略:写入 5 条但只保留最近 3 条
|
||||||
|
println!("=== SlidingWindow 策略(max_turns=3)===");
|
||||||
|
let store = Arc::new(InMemoryStore::new());
|
||||||
|
let config = ConversationMemoryConfig {
|
||||||
|
strategy: MemoryStrategy::SlidingWindow,
|
||||||
|
max_turns: 3,
|
||||||
|
compact_config: None,
|
||||||
|
};
|
||||||
|
let mut memory = ConversationMemory::new(store, "session-1", config);
|
||||||
|
|
||||||
|
for i in 0..5 {
|
||||||
|
memory
|
||||||
|
.add_message(Message::user_text(format!("消息 {i}")))
|
||||||
|
.await
|
||||||
|
.expect("写入失败");
|
||||||
|
}
|
||||||
|
println!("写入 5 条 → len = {} (期望 3)", memory.len());
|
||||||
|
let history = memory.get_history();
|
||||||
|
for (i, msg) in history.iter().enumerate() {
|
||||||
|
println!(" [{}] {}", i, message_text(msg));
|
||||||
|
}
|
||||||
|
assert_eq!(memory.len(), 3);
|
||||||
|
assert_eq!(message_text(&history[0]), "消息 2", "最旧应是消息 2");
|
||||||
|
assert_eq!(message_text(&history[2]), "消息 4", "最新应是消息 4");
|
||||||
|
|
||||||
|
// 2. Full 策略:保留全部
|
||||||
|
println!("\n=== Full 策略(max_turns=3)===");
|
||||||
|
let store2 = Arc::new(InMemoryStore::new());
|
||||||
|
let config2 = ConversationMemoryConfig {
|
||||||
|
strategy: MemoryStrategy::Full,
|
||||||
|
max_turns: 3,
|
||||||
|
compact_config: None,
|
||||||
|
};
|
||||||
|
let mut memory2 = ConversationMemory::new(store2, "session-2", config2);
|
||||||
|
for i in 0..5 {
|
||||||
|
memory2
|
||||||
|
.add_message(Message::user_text(format!("Full {i}")))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
println!("写入 5 条 → len = {} (期望 5)", memory2.len());
|
||||||
|
assert_eq!(memory2.len(), 5);
|
||||||
|
|
||||||
|
// 3. 多角色混合 + clear
|
||||||
|
println!("\n=== 多角色写入 + clear ===");
|
||||||
|
let store3 = Arc::new(InMemoryStore::new());
|
||||||
|
let mut memory3 = ConversationMemory::new(
|
||||||
|
store3,
|
||||||
|
"session-3",
|
||||||
|
ConversationMemoryConfig::default(),
|
||||||
|
);
|
||||||
|
memory3
|
||||||
|
.add_message(Message::user_text("你好"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
memory3
|
||||||
|
.add_message(Message::assistant("你好!有什么可以帮你的吗?"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
memory3
|
||||||
|
.add_message(Message::user_text("今天天气怎么样?"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
memory3
|
||||||
|
.add_message(Message::assistant("我无法查询实时天气,但你可以查看天气应用。"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
println!(
|
||||||
|
"写入 4 条多角色消息 → len = {}, 最后一条: {:?}",
|
||||||
|
memory3.len(),
|
||||||
|
message_text(memory3.get_history().last().unwrap())
|
||||||
|
);
|
||||||
|
assert_eq!(memory3.len(), 4);
|
||||||
|
|
||||||
|
memory3.clear().await.unwrap();
|
||||||
|
println!(
|
||||||
|
"clear 后 → len = {}, is_empty = {}",
|
||||||
|
memory3.len(),
|
||||||
|
memory3.is_empty()
|
||||||
|
);
|
||||||
|
assert!(memory3.is_empty());
|
||||||
|
|
||||||
|
// 4. Session 隔离
|
||||||
|
println!("\n=== Session 隔离(共用 InMemoryStore)===");
|
||||||
|
let store4 = Arc::new(InMemoryStore::new());
|
||||||
|
let mut a = ConversationMemory::new(
|
||||||
|
store4.clone(),
|
||||||
|
"s-a",
|
||||||
|
ConversationMemoryConfig::default(),
|
||||||
|
);
|
||||||
|
let mut b = ConversationMemory::new(
|
||||||
|
store4.clone(),
|
||||||
|
"s-b",
|
||||||
|
ConversationMemoryConfig::default(),
|
||||||
|
);
|
||||||
|
a.add_message(Message::user_text("A 的消息")).await.unwrap();
|
||||||
|
b.add_message(Message::user_text("B 的消息")).await.unwrap();
|
||||||
|
println!(
|
||||||
|
"A.len = {}, B.len = {} (期望 1/1,互不污染)",
|
||||||
|
a.len(),
|
||||||
|
b.len()
|
||||||
|
);
|
||||||
|
assert_eq!(a.len(), 1);
|
||||||
|
assert_eq!(b.len(), 1);
|
||||||
|
|
||||||
|
println!("\n✓ conversation_memory_demo 完成");
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
//! custom_tool —— 自定义工具注册、单次 / 并行调用、权限检查。
|
||||||
|
//!
|
||||||
|
//! 演示:
|
||||||
|
//! 1. 实现 `BaseTool` trait(WeatherTool + DeleteFileTool)
|
||||||
|
//! 2. 注册到 `ToolRegistry`
|
||||||
|
//! 3. 单次 `invoke`(含 tool_call_id 关联)
|
||||||
|
//! 4. 并行 `invoke_all`
|
||||||
|
//! 5. 调用未注册工具 → `ToolError::NotFound`
|
||||||
|
//! 6. `PermissionChecker` 黑名单阻断 `DeleteFileTool`
|
||||||
|
//!
|
||||||
|
//! 运行:`cargo run --example custom_tool`
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use agcore::tools::{
|
||||||
|
BaseTool, Permission, PermissionChecker, PermissionConfig, ToolContext, ToolError, ToolRef,
|
||||||
|
ToolRegistry,
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
/// 天气查询工具 —— 模拟根据城市返回天气数据。
|
||||||
|
struct WeatherTool;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl BaseTool for WeatherTool {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"get_weather"
|
||||||
|
}
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"查询指定城市的天气"
|
||||||
|
}
|
||||||
|
fn parameters(&self) -> Value {
|
||||||
|
json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"city": { "type": "string", "description": "城市名" }
|
||||||
|
},
|
||||||
|
"required": ["city"]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn required_permissions(&self) -> Vec<Permission> {
|
||||||
|
vec![Permission::Network]
|
||||||
|
}
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
args: Value,
|
||||||
|
_ctx: &ToolContext<'_>,
|
||||||
|
) -> Result<Value, ToolError> {
|
||||||
|
let city = args["city"].as_str().unwrap_or("未知");
|
||||||
|
// 模拟查询:根据城市名给出不同温度
|
||||||
|
let (temperature, condition) = match city {
|
||||||
|
"北京" => (22_i32, "晴"),
|
||||||
|
"上海" => (25, "多云"),
|
||||||
|
"广州" => (28, "雷阵雨"),
|
||||||
|
_ => (20, "晴"),
|
||||||
|
};
|
||||||
|
Ok(json!({
|
||||||
|
"city": city,
|
||||||
|
"temperature": temperature,
|
||||||
|
"condition": condition
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除文件工具 —— 用于演示权限黑名单阻断。
|
||||||
|
struct DeleteFileTool;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl BaseTool for DeleteFileTool {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"delete_file"
|
||||||
|
}
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"删除指定路径的文件"
|
||||||
|
}
|
||||||
|
fn parameters(&self) -> Value {
|
||||||
|
json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": { "path": { "type": "string" } },
|
||||||
|
"required": ["path"]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn required_permissions(&self) -> Vec<Permission> {
|
||||||
|
vec![Permission::Delete]
|
||||||
|
}
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_args: Value,
|
||||||
|
_ctx: &ToolContext<'_>,
|
||||||
|
) -> Result<Value, ToolError> {
|
||||||
|
Ok(json!({"deleted": true}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// 1. 注册工具
|
||||||
|
let mut registry = ToolRegistry::new();
|
||||||
|
let weather: ToolRef = Arc::new(WeatherTool);
|
||||||
|
let deleter: ToolRef = Arc::new(DeleteFileTool);
|
||||||
|
registry.register(weather).expect("注册 get_weather 失败");
|
||||||
|
registry.register(deleter).expect("注册 delete_file 失败");
|
||||||
|
println!("=== 已注册工具 ===");
|
||||||
|
println!("{:?}", registry.list_tools());
|
||||||
|
|
||||||
|
// 2. 单次 invoke:tool_call_id 用于回传时与原始调用关联
|
||||||
|
println!("\n=== 单次 invoke ===");
|
||||||
|
let result = registry
|
||||||
|
.invoke("call_1", "get_weather", json!({"city": "北京"}))
|
||||||
|
.await
|
||||||
|
.expect("invoke 失败");
|
||||||
|
println!("tool_call_id: {}", result.tool_call_id);
|
||||||
|
println!("tool_name: {}", result.tool_name);
|
||||||
|
println!("output: {}", result.output.unwrap());
|
||||||
|
|
||||||
|
// 3. 并行 invoke_all:三个并行天气查询
|
||||||
|
println!("\n=== 并行 invoke_all(30s 超时)===");
|
||||||
|
let calls = vec![
|
||||||
|
("c1".into(), "get_weather".into(), json!({"city": "北京"})),
|
||||||
|
("c2".into(), "get_weather".into(), json!({"city": "上海"})),
|
||||||
|
("c3".into(), "get_weather".into(), json!({"city": "广州"})),
|
||||||
|
];
|
||||||
|
let results = registry.invoke_all(calls, 30).await;
|
||||||
|
assert_eq!(results.len(), 3);
|
||||||
|
for r in &results {
|
||||||
|
let output = r.output.as_ref().unwrap();
|
||||||
|
println!("[{}] {}", r.tool_call_id, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 调用未注册工具
|
||||||
|
println!("\n=== 未注册工具 ===");
|
||||||
|
let err = registry
|
||||||
|
.invoke("c_x", "nope_tool", json!({}))
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
println!("错误: {err}");
|
||||||
|
|
||||||
|
// 5. 权限检查:默认 PermissionConfig 黑名单含 Delete
|
||||||
|
println!("\n=== 权限检查(默认 PermissionConfig,denied = [Delete, Shell])===");
|
||||||
|
let mut registry_with_checker = ToolRegistry::new().with_permission_checker(PermissionChecker::new(
|
||||||
|
PermissionConfig::default(),
|
||||||
|
));
|
||||||
|
registry_with_checker
|
||||||
|
.register(Arc::new(WeatherTool) as ToolRef)
|
||||||
|
.unwrap();
|
||||||
|
registry_with_checker
|
||||||
|
.register(Arc::new(DeleteFileTool) as ToolRef)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// get_weather 声明 Network → 在 allowed 列表 → 通过
|
||||||
|
let r = registry_with_checker
|
||||||
|
.invoke("c1", "get_weather", json!({"city": "北京"}))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
println!(
|
||||||
|
"get_weather 权限检查: {}",
|
||||||
|
if r.output.is_ok() { "通过 ✓" } else { "阻断 ✗" }
|
||||||
|
);
|
||||||
|
|
||||||
|
// delete_file 声明 Delete → 在 denied 列表 → 阻断
|
||||||
|
let err = registry_with_checker
|
||||||
|
.invoke("c2", "delete_file", json!({"path": "/tmp/x"}))
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
println!("delete_file 权限检查: 阻断 ✗ ({err})");
|
||||||
|
|
||||||
|
println!("\n✓ custom_tool 完成");
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
//! knowledge_search_demo —— 知识页面存储与关键词检索。
|
||||||
|
//!
|
||||||
|
//! 演示:
|
||||||
|
//! 1. `KnowledgeStore` 存储多个 `KnowledgePage`
|
||||||
|
//! 2. `MemoryRetriever` 按关键词检索 + TextOverlap (Dice) 评分
|
||||||
|
//! 3. 评分 [0.0, 1.0] 范围校验
|
||||||
|
//! 4. `RetrieverConfig::min_score` 阈值过滤
|
||||||
|
//! 5. `RetrieverConfig::max_results` 截断
|
||||||
|
//! 6. 空 query 返回空结果
|
||||||
|
//!
|
||||||
|
//! 运行:`cargo run --example knowledge_search_demo`
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use agcore::memory::{
|
||||||
|
InMemoryStore, KnowledgePage, KnowledgeStore, MemoryRetriever, MemoryStore, RetrieverConfig,
|
||||||
|
};
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
fn make_page(id: &str, title: &str, content: &str) -> KnowledgePage {
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
KnowledgePage {
|
||||||
|
id: id.to_string(),
|
||||||
|
title: title.to_string(),
|
||||||
|
summary: content.chars().take(30).collect(),
|
||||||
|
content: content.to_string(),
|
||||||
|
tags: Vec::new(),
|
||||||
|
references: Vec::new(),
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// 1. 创建知识库 + 批量存储页面
|
||||||
|
let store: Arc<dyn MemoryStore> = Arc::new(InMemoryStore::new());
|
||||||
|
let ks = KnowledgeStore::new(store);
|
||||||
|
|
||||||
|
let pages = vec![
|
||||||
|
make_page("rust-1", "Rust 入门", "Rust 是一门系统级编程语言,注重安全性与并发。"),
|
||||||
|
make_page("python-1", "Python 简介", "Python 是一门动态类型的高级编程语言。"),
|
||||||
|
make_page(
|
||||||
|
"langgraph-1",
|
||||||
|
"LangGraph 框架",
|
||||||
|
"LangGraph 是 LangChain 的状态图扩展,用于构建多步 Agent。",
|
||||||
|
),
|
||||||
|
make_page("rust-async", "Rust 异步编程", "Rust 异步基于 tokio 与 futures 抽象。"),
|
||||||
|
];
|
||||||
|
for p in &pages {
|
||||||
|
ks.add_page(p.clone()).await.expect("保存页面失败");
|
||||||
|
}
|
||||||
|
println!("=== 已存储 {} 个知识页面 ===", pages.len());
|
||||||
|
let index = ks.get_index().await;
|
||||||
|
for entry in &index {
|
||||||
|
println!(" - {} ({})", entry.title, entry.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 关键词检索 —— 期望命中 Rust 相关页面
|
||||||
|
println!("\n=== 关键词检索:'Rust 异步' ===");
|
||||||
|
let retriever = MemoryRetriever::new(ks, RetrieverConfig::default());
|
||||||
|
let result = retriever.retrieve("Rust 异步").await.unwrap();
|
||||||
|
println!("query: {}", result.query);
|
||||||
|
for item in &result.items {
|
||||||
|
println!(
|
||||||
|
" 命中: {} (score={:.3})",
|
||||||
|
item.page.title, item.score
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(0.0..=1.0).contains(&item.score),
|
||||||
|
"score 应在 [0, 1] 区间"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(!result.items.is_empty(), "应至少命中一个页面");
|
||||||
|
|
||||||
|
// 3. min_score 阈值过滤
|
||||||
|
println!("\n=== min_score=0.5 阈值过滤(无关 query)===");
|
||||||
|
let store2: Arc<dyn MemoryStore> = Arc::new(InMemoryStore::new());
|
||||||
|
let ks2 = KnowledgeStore::new(store2);
|
||||||
|
ks2.add_page(make_page("rust-1", "Rust 入门", "Rust 入门内容。"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let cfg = RetrieverConfig {
|
||||||
|
max_results: 20,
|
||||||
|
min_score: 0.5,
|
||||||
|
};
|
||||||
|
let retriever2 = MemoryRetriever::new(ks2, cfg);
|
||||||
|
let result = retriever2
|
||||||
|
.retrieve("完全不相关的火锅配方")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
println!(
|
||||||
|
"无关 query → items.len = {} (期望 0)",
|
||||||
|
result.items.len()
|
||||||
|
);
|
||||||
|
assert!(result.items.is_empty());
|
||||||
|
|
||||||
|
// 4. max_results 截断
|
||||||
|
println!("\n=== max_results=2 截断 ===");
|
||||||
|
let store3: Arc<dyn MemoryStore> = Arc::new(InMemoryStore::new());
|
||||||
|
let ks3 = KnowledgeStore::new(store3);
|
||||||
|
for i in 0..5 {
|
||||||
|
ks3.add_page(make_page(
|
||||||
|
&format!("rust-{i}"),
|
||||||
|
"Rust 主题",
|
||||||
|
&format!("第 {i} 篇关于 Rust 的内容"),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let cfg = RetrieverConfig {
|
||||||
|
max_results: 2,
|
||||||
|
min_score: 0.0,
|
||||||
|
};
|
||||||
|
let retriever3 = MemoryRetriever::new(ks3, cfg);
|
||||||
|
let result = retriever3.retrieve("Rust").await.unwrap();
|
||||||
|
println!(
|
||||||
|
"5 个相关页面 → 返回 items.len = {} (期望 2)",
|
||||||
|
result.items.len()
|
||||||
|
);
|
||||||
|
assert_eq!(result.items.len(), 2);
|
||||||
|
|
||||||
|
// 5. 空 query
|
||||||
|
println!("\n=== 空 query ===");
|
||||||
|
let empty = retriever3.retrieve("").await.unwrap();
|
||||||
|
println!("空 query → items.len = {}", empty.items.len());
|
||||||
|
assert!(empty.items.is_empty());
|
||||||
|
|
||||||
|
// 6. 停用词过滤:`extract_keywords` 在检索前过滤单字符词与停用词
|
||||||
|
println!("\n=== 停用词过滤 ===");
|
||||||
|
let mixed = retriever3.retrieve("the Rust is").await.unwrap();
|
||||||
|
println!(
|
||||||
|
"query='the Rust is' → 命中 {} 个 (停用词 'the'/'is' 被过滤,仅 'rust' 进入搜索)",
|
||||||
|
mixed.items.len()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!mixed.items.is_empty(),
|
||||||
|
"非停用词 'rust' 应命中页面(即使 query 中含停用词)"
|
||||||
|
);
|
||||||
|
|
||||||
|
let only_stop = retriever3.retrieve("the is are").await.unwrap();
|
||||||
|
println!(
|
||||||
|
"纯停用词 query='the is are' → 命中 {} 个 (期望 0,所有 token 均被过滤)",
|
||||||
|
only_stop.items.len()
|
||||||
|
);
|
||||||
|
assert!(only_stop.items.is_empty(), "纯停用词 query 必须返回空结果");
|
||||||
|
|
||||||
|
println!("\n✓ knowledge_search_demo 完成");
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
//! prompt_composer —— 提示词模板与组合器离线示例。
|
||||||
|
//!
|
||||||
|
//! 演示:
|
||||||
|
//! 1. `PromptTemplate::compile` + `render` 变量插值(`{{var}}` 语法)
|
||||||
|
//! 2. 缺失变量返回 `PromptError`
|
||||||
|
//! 3. `PromptTemplateRegistry` 注册 + 按名渲染
|
||||||
|
//! 4. `PromptComposer` 构造多角色消息序列
|
||||||
|
//! 5. `validate_messages` 校验消息序列合法性
|
||||||
|
//!
|
||||||
|
//! 运行:`cargo run --example prompt_composer`
|
||||||
|
|
||||||
|
use agcore::llm::types::message::{ContentBlock, Message};
|
||||||
|
use agcore::prompt::{
|
||||||
|
validate_messages, PromptComposer, PromptTemplate, PromptTemplateRegistry, TemplateContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn message_text(msg: &Message) -> String {
|
||||||
|
match msg {
|
||||||
|
Message::System { content }
|
||||||
|
| Message::User { content }
|
||||||
|
| Message::Assistant { content }
|
||||||
|
| Message::ToolResult { content, .. } => content
|
||||||
|
.iter()
|
||||||
|
.filter_map(|b| match b {
|
||||||
|
ContentBlock::Text { text } => Some(text.as_str()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
Message::UserImage { .. } => "[image]".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// 1. PromptTemplate::compile + render —— 直接构造模板
|
||||||
|
println!("=== PromptTemplate::compile + render ===");
|
||||||
|
let tpl = PromptTemplate::compile(
|
||||||
|
"今日 {{location}} 天气:{{condition}},温度 {{temperature}}",
|
||||||
|
)
|
||||||
|
.expect("编译失败");
|
||||||
|
let mut ctx = TemplateContext::new();
|
||||||
|
ctx.insert("location", "北京");
|
||||||
|
ctx.insert("condition", "晴");
|
||||||
|
ctx.insert("temperature", "25°C");
|
||||||
|
let rendered = tpl.render(&ctx).expect("渲染失败");
|
||||||
|
println!("渲染结果: {rendered}");
|
||||||
|
|
||||||
|
// 2. 缺失变量 → PromptError
|
||||||
|
println!("\n=== 缺失变量 ===");
|
||||||
|
match tpl.render(&TemplateContext::new()) {
|
||||||
|
Ok(_) => println!("意外成功"),
|
||||||
|
Err(e) => println!("按预期报错: {e}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. PromptTemplateRegistry —— 按名注册 + 渲染(支持 #if 条件)
|
||||||
|
println!("\n=== PromptTemplateRegistry ===");
|
||||||
|
let mut registry = PromptTemplateRegistry::new();
|
||||||
|
registry
|
||||||
|
.register("weather", "今日 {{location}}:{{condition}}")
|
||||||
|
.expect("注册失败");
|
||||||
|
registry
|
||||||
|
.register("greet", "你好 {{name}}!{{#if formal}} 见到您很荣幸。{{/if}}")
|
||||||
|
.expect("注册失败");
|
||||||
|
|
||||||
|
let mut ctx = TemplateContext::new();
|
||||||
|
ctx.insert("name", "Alice");
|
||||||
|
println!(
|
||||||
|
"greet (formal=false): {}",
|
||||||
|
registry.render("greet", &ctx).unwrap()
|
||||||
|
);
|
||||||
|
ctx.insert("formal", true);
|
||||||
|
println!(
|
||||||
|
"greet (formal=true): {}",
|
||||||
|
registry.render("greet", &ctx).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. PromptComposer —— 构造多角色消息序列
|
||||||
|
println!("\n=== PromptComposer ===");
|
||||||
|
let messages = PromptComposer::new()
|
||||||
|
.system("你是一个天气助手")
|
||||||
|
.user("今天天气怎么样?")
|
||||||
|
.assistant("请告诉我城市名。")
|
||||||
|
.user(rendered)
|
||||||
|
.build();
|
||||||
|
println!("消息数: {}", messages.len());
|
||||||
|
for (i, m) in messages.iter().enumerate() {
|
||||||
|
let role = match m {
|
||||||
|
Message::System { .. } => "system",
|
||||||
|
Message::User { .. } | Message::UserImage { .. } => "user",
|
||||||
|
Message::Assistant { .. } => "assistant",
|
||||||
|
Message::ToolResult { .. } => "tool",
|
||||||
|
};
|
||||||
|
println!("[{i}] {role}: {}", message_text(m));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. validate_messages —— 消息序列合法性校验
|
||||||
|
println!("\n=== validate_messages ===");
|
||||||
|
match validate_messages(&messages) {
|
||||||
|
Ok(()) => println!("消息序列合法 ✓"),
|
||||||
|
Err(e) => println!("消息序列非法: {e}"),
|
||||||
|
}
|
||||||
|
let empty: Vec<Message> = Vec::new();
|
||||||
|
match validate_messages(&empty) {
|
||||||
|
Ok(()) => println!("空消息合法"),
|
||||||
|
Err(e) => println!("空消息按预期报错: {e}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n✓ prompt_composer 完成");
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
//! streaming_events_demo —— LLM 流式响应事件流消费(含错误路径)。
|
||||||
|
//!
|
||||||
|
//! 演示:
|
||||||
|
//! 1. `MockProvider::chat_stream` 输出标准 `StreamEvent` 流(离线可跑)
|
||||||
|
//! 2. `LlmCycle::submit_stream` 消费流
|
||||||
|
//! 3. match 各类 `StreamEvent`:MessageStart / ContentBlockStart / TextDelta /
|
||||||
|
//! ContentBlockEnd / CostUpdate / MessageComplete
|
||||||
|
//! 4. 实时累计文本与解析事件计数
|
||||||
|
//! 5. 从 `MessageComplete` 拿到完整 `MessageResponse`
|
||||||
|
//! 6. **错误路径**:队列耗尽时 `chat_stream` 返回 `Err`,`submit_stream` 同样
|
||||||
|
//! 返回 `Err`(错误不进流,直接 fail-fast)
|
||||||
|
//!
|
||||||
|
//! 运行:`cargo run --example streaming_events_demo`
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use agcore::llm::cycle::{CycleConfig, LlmCycle};
|
||||||
|
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, StreamEvent};
|
||||||
|
use agcore::llm::types::Usage;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
|
||||||
|
/// 构造预设的纯文本响应。
|
||||||
|
fn text_response(id: &str, text: &str) -> MessageResponse {
|
||||||
|
MessageResponse {
|
||||||
|
id: id.to_string(),
|
||||||
|
model: "mock".into(),
|
||||||
|
message: Message::Assistant {
|
||||||
|
content: vec![ContentBlock::Text { text: text.into() }],
|
||||||
|
},
|
||||||
|
usage: Usage::from_input_output(3, text.chars().count() as u32),
|
||||||
|
stop_reason: StopReason::Stop,
|
||||||
|
extra: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 消费一个流直到终止事件,并打印事件轨迹。
|
||||||
|
async fn drain_stream(stream: &mut (impl futures_util::Stream<Item = StreamEvent> + Unpin)) {
|
||||||
|
while let Some(event) = stream.next().await {
|
||||||
|
match event {
|
||||||
|
StreamEvent::MessageStart { id, model } => {
|
||||||
|
println!("[MessageStart] id={id}, model={model}");
|
||||||
|
}
|
||||||
|
StreamEvent::ContentBlockStart { index, .. } => {
|
||||||
|
println!("[ContentBlockStart] index={index}");
|
||||||
|
}
|
||||||
|
StreamEvent::TextDelta { text } => {
|
||||||
|
print!("[TextDelta] {text}");
|
||||||
|
use std::io::Write;
|
||||||
|
std::io::stdout().flush().ok();
|
||||||
|
}
|
||||||
|
StreamEvent::ContentBlockEnd { index } => {
|
||||||
|
println!("\n[ContentBlockEnd] index={index}");
|
||||||
|
}
|
||||||
|
StreamEvent::CostUpdate { usage } => {
|
||||||
|
let p = usage.prompt_tokens.unwrap_or(0);
|
||||||
|
let c = usage.completion_tokens.unwrap_or(0);
|
||||||
|
println!("[CostUpdate] prompt={p}, completion={c}");
|
||||||
|
}
|
||||||
|
StreamEvent::MessageComplete { full_response } => {
|
||||||
|
println!(
|
||||||
|
"[MessageComplete] stop_reason={:?}",
|
||||||
|
full_response.stop_reason
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
// 未匹配的事件变体(ThinkingDelta / RefusalDelta / ToolCall* 等)
|
||||||
|
// 在 MockProvider 当前实现下不可达;保留打印以便未来 Provider 扩展时易调试。
|
||||||
|
eprintln!("[unhandled event] {other:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// 1. 准备 Mock provider(仅含一轮文本响应,第二次调用会触发错误)
|
||||||
|
let provider = Arc::new(MockProvider::new(vec![text_response(
|
||||||
|
"resp-001",
|
||||||
|
"今天天气晴朗,适合户外活动。",
|
||||||
|
)]));
|
||||||
|
let dyn_provider: Arc<dyn LlmProvider> = provider.clone();
|
||||||
|
|
||||||
|
// ===== 阶段 1:正常流式响应 =====
|
||||||
|
println!("=== 阶段 1:正常流式响应 ===");
|
||||||
|
let mut cycle = LlmCycle::new_with_arc(dyn_provider.clone(), CycleConfig::default());
|
||||||
|
let mut stream = cycle
|
||||||
|
.submit_stream("讲个笑话".to_string(), vec![])
|
||||||
|
.await
|
||||||
|
.expect("阶段 1 submit_stream 应成功");
|
||||||
|
drain_stream(&mut stream).await;
|
||||||
|
|
||||||
|
// ===== 阶段 2:错误路径(队列耗尽)=====
|
||||||
|
// 队列中已无响应 → `chat_stream` 返回 `Err(LlmError::Other)` →
|
||||||
|
// `submit_stream` 用 `?` 立即传播(错误不进流,fail-fast)。
|
||||||
|
// 上层 Agent 通过 `match` 或 `?` 处理 `AgentError::Llm(_)`。
|
||||||
|
println!("\n=== 阶段 2:错误路径(队列耗尽)===");
|
||||||
|
let mut cycle = LlmCycle::new_with_arc(dyn_provider, CycleConfig::default());
|
||||||
|
let result = cycle
|
||||||
|
.submit_stream("第二次提问".to_string(), vec![])
|
||||||
|
.await;
|
||||||
|
match result {
|
||||||
|
Ok(_) => panic!("阶段 2 必须失败(队列耗尽)"),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[expected error] {e}");
|
||||||
|
assert!(
|
||||||
|
e.to_string().contains("预设响应已用完"),
|
||||||
|
"应包含 MockProvider 的队列耗尽提示"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n✓ streaming_events_demo 完成");
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
//! task_agent_demo —— Plan 解析、Step 状态机、错误路径。
|
||||||
|
//!
|
||||||
|
//! 演示:
|
||||||
|
//! 1. `JsonPlanParser::parse` 解析合法 JSON 输入
|
||||||
|
//! 2. `Plan` / `Step` 数据结构遍历
|
||||||
|
//! 3. `StepStatus` 状态机:Pending → Running → Completed / Failed / Skipped
|
||||||
|
//! 4. `is_pending()` / `is_terminal()` 语义
|
||||||
|
//! 5. 错误路径:非法 JSON / 空 steps / 缺字段 → `AgentError::PlanParse`
|
||||||
|
//!
|
||||||
|
//! 运行:`cargo run --example task_agent_demo`
|
||||||
|
//!
|
||||||
|
//! ## 已知技术债(v0.2 迁移指南)
|
||||||
|
//!
|
||||||
|
//! 本示例使用 `#[deprecated]` 标记的旧 wire-format 类型:
|
||||||
|
//! - `ChatResponse`、`OpenaiChatMessage`、`FinishReason` —— `OpenaiChatProvider::chat_inner()`
|
||||||
|
//! 内部转换层仍在使用(参见 `docs/10a-phase0-types-and-trait.md` §2.5.1),
|
||||||
|
//! 故结构体定义保留。
|
||||||
|
//! - `StepStatus::Completed(ChatResponse)` —— 因为 `Step` 的"已完成"变体需携带
|
||||||
|
//! provider 响应,目前沿用旧的 `ChatResponse`。
|
||||||
|
//!
|
||||||
|
//! **触发迁移的条件**:v0.2 引入 IR 层的 `StepResult` / 切换为 `MessageResponse`。
|
||||||
|
//! **迁移路径**:将本文件 `ChatResponse`/`OpenaiChatMessage`/`FinishReason` 替换为
|
||||||
|
//! `MessageResponse`/`Message`/`StopReason`,移除顶部 `#![allow(deprecated)]`。
|
||||||
|
//! 上层应用代码(`TaskAgent` 消费者)也可同步迁移。
|
||||||
|
|
||||||
|
#![allow(deprecated)]
|
||||||
|
|
||||||
|
use agcore::agent::{AgentError, JsonPlanParser, PlanParser, Step, StepStatus};
|
||||||
|
use agcore::llm::types::openai_message::OpenaiChatMessage;
|
||||||
|
use agcore::llm::types::shared::FinishReason;
|
||||||
|
use agcore::llm::types::{ChatResponse, Usage};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// 1. JsonPlanParser 解析合法 JSON
|
||||||
|
println!("=== JsonPlanParser 解析合法 JSON ===");
|
||||||
|
let parser = JsonPlanParser;
|
||||||
|
let input = r#"{
|
||||||
|
"steps": [
|
||||||
|
{"description": "查询北京天气"},
|
||||||
|
{"description": "根据天气计算穿衣建议"},
|
||||||
|
{"description": "生成最终回复"}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
let plan = parser
|
||||||
|
.parse(input, "为用户生成今日出行建议")
|
||||||
|
.await
|
||||||
|
.expect("合法 JSON 应解析成功");
|
||||||
|
println!("Plan goal: {}", plan.goal);
|
||||||
|
println!("Plan id: {}", plan.id);
|
||||||
|
println!("Steps: {}", plan.steps.len());
|
||||||
|
for (i, step) in plan.steps.iter().enumerate() {
|
||||||
|
println!(
|
||||||
|
" [{}] {} (pending={}, terminal={})",
|
||||||
|
i,
|
||||||
|
step.description,
|
||||||
|
step.status.is_pending(),
|
||||||
|
step.status.is_terminal()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(plan.steps.iter().all(|s| s.status.is_pending()));
|
||||||
|
|
||||||
|
// 2. Step 状态机:Pending → Running → Completed
|
||||||
|
println!("\n=== Step 状态机:Pending → Running → Completed ===");
|
||||||
|
let mut step = plan.steps.into_iter().next().expect("应有 step");
|
||||||
|
println!("初始: pending={}", step.status.is_pending());
|
||||||
|
assert!(step.status.is_pending());
|
||||||
|
|
||||||
|
step.status = StepStatus::Running;
|
||||||
|
println!("Running: pending={}, terminal={}", step.status.is_pending(), step.status.is_terminal());
|
||||||
|
|
||||||
|
step.status = StepStatus::Completed(ChatResponse {
|
||||||
|
message: OpenaiChatMessage::assistant_text("天气:晴,22°C"),
|
||||||
|
usage: Usage::from_input_output(5, 10),
|
||||||
|
stop_reason: Some(FinishReason::Stop),
|
||||||
|
});
|
||||||
|
println!("Completed: pending={}, terminal={}", step.status.is_pending(), step.status.is_terminal());
|
||||||
|
assert!(step.status.is_terminal());
|
||||||
|
|
||||||
|
// 3. 失败路径
|
||||||
|
println!("\n=== Step 状态机:失败路径 ===");
|
||||||
|
let mut fail_step = Step::new(0, "调用天气 API");
|
||||||
|
fail_step.status = StepStatus::Failed(AgentError::Other("API 不可用".into()));
|
||||||
|
println!("Failed: pending={}, terminal={}", fail_step.status.is_pending(), fail_step.status.is_terminal());
|
||||||
|
assert!(fail_step.status.is_terminal());
|
||||||
|
|
||||||
|
// 4. 跳过路径
|
||||||
|
let mut skipped = Step::new(0, "可选步骤");
|
||||||
|
skipped.status = StepStatus::Skipped;
|
||||||
|
println!("Skipped: terminal={}", skipped.status.is_terminal());
|
||||||
|
assert!(skipped.status.is_terminal());
|
||||||
|
|
||||||
|
// 5. 错误路径 1:非法 JSON
|
||||||
|
println!("\n=== 错误路径:非法 JSON ===");
|
||||||
|
let err = parser.parse("not json", "goal").await.unwrap_err();
|
||||||
|
println!("错误: {err}");
|
||||||
|
assert!(matches!(err, AgentError::PlanParse(_)));
|
||||||
|
|
||||||
|
// 6. 错误路径 2:空 steps
|
||||||
|
println!("\n=== 错误路径:空 steps ===");
|
||||||
|
let err = parser.parse(r#"{"steps": []}"#, "goal").await.unwrap_err();
|
||||||
|
println!("错误: {err}");
|
||||||
|
assert!(matches!(err, AgentError::PlanParse(_)));
|
||||||
|
|
||||||
|
// 7. 错误路径 3:缺 description 字段
|
||||||
|
println!("\n=== 错误路径:缺 description 字段 ===");
|
||||||
|
let bad_input = r#"{"steps": [{"description": "ok"}, {"no_desc": true}]}"#;
|
||||||
|
let err = parser.parse(bad_input, "goal").await.unwrap_err();
|
||||||
|
println!("错误: {err}");
|
||||||
|
assert!(matches!(err, AgentError::PlanParse(_)));
|
||||||
|
|
||||||
|
println!("\n✓ task_agent_demo 完成");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user