af6cfdaa83
- 新增 daemon/channel/assistant/function/scheduler/logging worker 架构和 JetStream 消息协议 - 修复邮件通知误入 assistant、迁移 checksum、日志 subject 和工具调用无限循环风险 - 新增 assistant 工具轮次熔断、工具结果截断和提示词软收敛规则 - 补充邮件推送关键字、可信发件人配置、升级日志和验证测试
159 lines
5.6 KiB
Rust
159 lines
5.6 KiB
Rust
use regex::Regex;
|
|
use std::env;
|
|
|
|
use ias_common::model::ai::{
|
|
ChatCompletionRequestBody, ChatCompletionRequestExtra, ChatCompletionRequestMessage,
|
|
ChatCompletionRequestThinking, ChatCompletionResponseBody,
|
|
};
|
|
use ias_common::queue::{AssistantMessage, ChannelMessage, QueueMessage};
|
|
|
|
use crate::toolkit::{call_tools, get_tool, tool_notice_text};
|
|
|
|
pub async fn send_message(
|
|
sender: tokio::sync::mpsc::Sender<QueueMessage>,
|
|
messages: Vec<ChatCompletionRequestMessage>,
|
|
channel: ChannelMessage,
|
|
) -> anyhow::Result<()> {
|
|
let result = request_llm(messages).await;
|
|
match result {
|
|
Ok(result) => {
|
|
let Some(result) = result else {
|
|
eprintln!("LLM 请求没有返回有效响应体");
|
|
return Ok(());
|
|
};
|
|
|
|
for choice in result.choices {
|
|
let raw_message = serde_json::to_value(&choice.message).ok();
|
|
let messsage_content = choice.message.content.clone().unwrap_or_default();
|
|
let messsage_reasoning = choice.message.reasoning_content.unwrap_or_default();
|
|
let tool_calls = choice.message.tool_calls.clone();
|
|
|
|
sender
|
|
.send(QueueMessage::Assistant(
|
|
channel.clone(),
|
|
AssistantMessage {
|
|
reasoning: Some(messsage_reasoning),
|
|
content: messsage_content,
|
|
tool_calls,
|
|
raw_message,
|
|
},
|
|
))
|
|
.await
|
|
.unwrap();
|
|
|
|
match choice.message.tool_calls {
|
|
Some(tool_calls) => {
|
|
if tool_calls.is_empty() {
|
|
eprintln!("模型返回 tool_calls 字段,但列表为空");
|
|
continue;
|
|
}
|
|
|
|
println!("模型请求调用{}个工具", tool_calls.len());
|
|
if let Some(text) = tool_notice_text(&tool_calls) {
|
|
sender
|
|
.send(QueueMessage::Notice(channel.clone(), text))
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
match call_tools(channel.clone(), tool_calls).await {
|
|
Ok(Some(queue_message)) => {
|
|
sender.send(queue_message).await.unwrap();
|
|
}
|
|
Ok(None) => {
|
|
eprintln!("工具调用没有生成后续队列消息");
|
|
}
|
|
Err(error) => {
|
|
eprintln!("工具调用失败: {error}");
|
|
}
|
|
}
|
|
}
|
|
None => {
|
|
eprintln!("没有调用工具");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
eprintln!("发送错误:{}", err);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn request_llm(
|
|
messages: Vec<ChatCompletionRequestMessage>,
|
|
) -> anyhow::Result<Option<ChatCompletionResponseBody>> {
|
|
let client = reqwest::Client::new();
|
|
let base_url =
|
|
env::var("PROVIDER_BASE_URL").map_err(|_| anyhow::anyhow!("必须设置PROVIDER_BASE_URL"))?;
|
|
let chat_re = Regex::new(r"(?i)(?:^|/)(?:v\d+/)?chat/completions/?$")?;
|
|
let version_re = Regex::new(r"(?i)(?:^|/)v\d+$")?;
|
|
|
|
let request_url = if chat_re.is_match(&base_url) {
|
|
base_url.to_string()
|
|
} else if version_re.is_match(&base_url) {
|
|
format!("{}/chat/completions", base_url)
|
|
} else {
|
|
format!("{}/v1/chat/completions", base_url)
|
|
};
|
|
let api_key =
|
|
env::var("PROVIDER_API_KEY").map_err(|_| anyhow::anyhow!("必须设置PROVIDER_API_KEY"))?;
|
|
let model_name = env::var("PROVIDER_MODEL").unwrap_or_else(|_| "deepseek-v4-flash".to_string());
|
|
let body = ChatCompletionRequestBody {
|
|
model: model_name,
|
|
messages,
|
|
stream: false,
|
|
tools: get_tool(),
|
|
extra_body: Some(ChatCompletionRequestExtra {
|
|
thinking: ChatCompletionRequestThinking {
|
|
thinking_type: "enabled".to_string(),
|
|
},
|
|
}),
|
|
};
|
|
let resp = client
|
|
.post(request_url)
|
|
.bearer_auth(api_key)
|
|
.json(&body)
|
|
.send()
|
|
.await;
|
|
|
|
match resp {
|
|
Ok(resp) => {
|
|
let status = resp.status();
|
|
let text = resp.text().await.unwrap_or_default();
|
|
// println!("response:\n{}", text);
|
|
if status.is_success() {
|
|
match serde_json::from_str::<ChatCompletionResponseBody>(&text) {
|
|
Ok(result) => return Ok(Some(result)),
|
|
Err(error) => {
|
|
eprintln!("LLM 响应解析失败: {error}");
|
|
eprintln!("body: {text}");
|
|
}
|
|
}
|
|
} else {
|
|
eprintln!("HTTP 请求失败");
|
|
eprintln!("status: {}", status);
|
|
eprintln!("body: {}", text);
|
|
}
|
|
}
|
|
Err(err) => {
|
|
eprintln!("请求发送失败: {}", err);
|
|
|
|
if err.is_timeout() {
|
|
eprintln!("原因: 请求超时");
|
|
}
|
|
|
|
if err.is_connect() {
|
|
eprintln!("原因: 连接失败");
|
|
}
|
|
|
|
if let Some(url) = err.url() {
|
|
eprintln!("url: {}", url);
|
|
}
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|