feat: 拆分 worker 架构并增强 assistant 安全保护
- 新增 daemon/channel/assistant/function/scheduler/logging worker 架构和 JetStream 消息协议 - 修复邮件通知误入 assistant、迁移 checksum、日志 subject 和工具调用无限循环风险 - 新增 assistant 工具轮次熔断、工具结果截断和提示词软收敛规则 - 补充邮件推送关键字、可信发件人配置、升级日志和验证测试
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "ias-function"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
ias-ai = { path = "../ias-ai" }
|
||||
ias-common = { path = "../ias-common" }
|
||||
anyhow = "1"
|
||||
serde_json = "1"
|
||||
tokio = { version = "1.0", features = ["rt-multi-thread", "macros", "time"] }
|
||||
@@ -0,0 +1,118 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use ias_ai::toolkit::call_tools;
|
||||
use ias_common::model::ai::{ChatCompletionFunction, ChatCompletionToolCall};
|
||||
use ias_common::mq::{
|
||||
ConsumerSpec, FunctionCallPayload, FunctionResultPayload, JetStreamBus, JetStreamConfig,
|
||||
MessageEnvelope, StreamKind, StreamNames, function_result_subject,
|
||||
};
|
||||
use ias_common::queue::QueueMessage;
|
||||
|
||||
pub async fn run_worker(name: Option<String>) -> anyhow::Result<()> {
|
||||
let worker_name = name.unwrap_or_else(|| "default".to_string());
|
||||
let config = JetStreamConfig::from_env();
|
||||
loop {
|
||||
match run_once(worker_name.clone(), config.clone()).await {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(error) => {
|
||||
eprintln!("Function Worker 连接或消费失败: {error:#}");
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_once(worker_name: String, config: JetStreamConfig) -> anyhow::Result<()> {
|
||||
let bus = JetStreamBus::connect(config.clone()).await?;
|
||||
bus.ensure_streams().await?;
|
||||
let names = StreamNames::new(config.stream_prefix.clone());
|
||||
let spec = ConsumerSpec {
|
||||
stream: names.name(StreamKind::Function),
|
||||
durable_name: format!("function-worker-{worker_name}"),
|
||||
filter_subject: "function.call.>".to_string(),
|
||||
ack_wait: config.ack_wait(),
|
||||
max_deliver: config.max_deliver,
|
||||
};
|
||||
let publish_bus = bus.clone();
|
||||
bus.consume_envelopes::<FunctionCallPayload, _, _>(spec, move |envelope| {
|
||||
let bus = publish_bus.clone();
|
||||
async move {
|
||||
let call = envelope.payload;
|
||||
let tool_call = ChatCompletionToolCall {
|
||||
id: call.call_id.clone(),
|
||||
call_type: "function".to_string(),
|
||||
function: Some(ChatCompletionFunction {
|
||||
name: call.name.clone(),
|
||||
arguments: call.arguments.clone(),
|
||||
}),
|
||||
};
|
||||
let result = call_tools(call.channel.clone(), vec![tool_call]).await?;
|
||||
let (name, result) = match result {
|
||||
Some(QueueMessage::Tools(_, tools)) => {
|
||||
let tool = tools
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("工具执行没有返回结果"))?;
|
||||
(tool.name, tool.result)
|
||||
}
|
||||
Some(QueueMessage::Tool(_, _, name, result)) => (name, result),
|
||||
Some(_) => anyhow::bail!("工具执行返回了非工具消息"),
|
||||
None => anyhow::bail!("工具执行没有生成队列消息"),
|
||||
};
|
||||
let payload = FunctionResultPayload {
|
||||
call_id: call.call_id.clone(),
|
||||
name,
|
||||
result,
|
||||
channel: call.channel,
|
||||
};
|
||||
let subject = function_result_subject(&payload.call_id);
|
||||
let envelope = MessageEnvelope::new(
|
||||
subject.as_str(),
|
||||
format!("function-result/{}", payload.call_id),
|
||||
envelope.trace_id,
|
||||
payload,
|
||||
);
|
||||
bus.publish_envelope(subject.as_str(), &envelope).await?;
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ias_common::mq::{FunctionCallPayload, MessageEnvelope, function_call_subject};
|
||||
use ias_common::queue::ChannelMessage;
|
||||
|
||||
#[test]
|
||||
fn function_call_payload_keeps_call_id_as_idempotency_key() {
|
||||
let payload = FunctionCallPayload {
|
||||
call_id: "call-1".to_string(),
|
||||
name: "query_capabilities".to_string(),
|
||||
arguments: "{}".to_string(),
|
||||
channel: ChannelMessage {
|
||||
channel: "wechat".to_string(),
|
||||
account_id: "acct".to_string(),
|
||||
from_user_id: "user".to_string(),
|
||||
content: "hello".to_string(),
|
||||
context: None,
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
external_message_id: None,
|
||||
session_id: None,
|
||||
group_id: None,
|
||||
create_time_ms: None,
|
||||
},
|
||||
};
|
||||
let subject = function_call_subject(&payload.name);
|
||||
let envelope = MessageEnvelope::new(
|
||||
subject.as_str(),
|
||||
format!("function-call/{}", payload.call_id),
|
||||
"trace-1",
|
||||
payload,
|
||||
);
|
||||
let encoded = serde_json::to_string(&envelope).unwrap();
|
||||
|
||||
assert!(encoded.contains("function-call/call-1"));
|
||||
assert!(encoded.contains("trace-1"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user