feat: add React UI and AI page sharing tools
Move HTTP page rendering to the React app, fix multi-tool-call message handling, add API docs lookup, web page lookup, and Markdown preview page sharing.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ias-context"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+132
-2
@@ -6,6 +6,7 @@ use ias_common::model::ai::{
|
||||
};
|
||||
use ias_common::queue::ChannelMessage;
|
||||
use sqlx::PgPool;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::model::{ContextMessageRow, ContextScope, ContextSession, LongTermMemory};
|
||||
use crate::repository::ContextRepository;
|
||||
@@ -112,6 +113,7 @@ impl ContextManager {
|
||||
let session = self.active_or_new_session(&scope).await?;
|
||||
let tool_calls = tool_calls
|
||||
.as_ref()
|
||||
.filter(|calls| !calls.is_empty())
|
||||
.map(serde_json::to_string)
|
||||
.transpose()
|
||||
.context("序列化 assistant tool_calls 失败")?;
|
||||
@@ -285,6 +287,7 @@ impl ContextManager {
|
||||
messages.push(row_to_chat_message(row)?);
|
||||
}
|
||||
|
||||
let messages = sanitize_tool_call_messages(messages);
|
||||
Ok(ContextSnapshot { messages })
|
||||
}
|
||||
|
||||
@@ -448,7 +451,8 @@ fn row_to_chat_message(row: ContextMessageRow) -> anyhow::Result<ChatCompletionR
|
||||
.as_deref()
|
||||
.map(serde_json::from_str::<Vec<ChatCompletionToolCall>>)
|
||||
.transpose()
|
||||
.context("解析历史 tool_calls 失败")?;
|
||||
.context("解析历史 tool_calls 失败")?
|
||||
.filter(|calls| !calls.is_empty());
|
||||
|
||||
Ok(ChatCompletionRequestMessage {
|
||||
role: row.role,
|
||||
@@ -458,6 +462,74 @@ fn row_to_chat_message(row: ContextMessageRow) -> anyhow::Result<ChatCompletionR
|
||||
})
|
||||
}
|
||||
|
||||
fn sanitize_tool_call_messages(
|
||||
messages: Vec<ChatCompletionRequestMessage>,
|
||||
) -> Vec<ChatCompletionRequestMessage> {
|
||||
let mut sanitized = Vec::with_capacity(messages.len());
|
||||
let mut index = 0;
|
||||
|
||||
while index < messages.len() {
|
||||
let message = &messages[index];
|
||||
let tool_calls = message
|
||||
.tool_calls
|
||||
.as_ref()
|
||||
.filter(|calls| !calls.is_empty());
|
||||
|
||||
if message.role == "assistant"
|
||||
&& let Some(tool_calls) = tool_calls
|
||||
{
|
||||
let expected_ids = tool_calls
|
||||
.iter()
|
||||
.map(|call| call.id.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
let mut seen_ids = HashSet::new();
|
||||
let mut tool_messages = Vec::new();
|
||||
let mut next = index + 1;
|
||||
|
||||
while next < messages.len() && messages[next].role == "tool" {
|
||||
if let Some(tool_call_id) = messages[next].tool_call_id.as_deref()
|
||||
&& expected_ids.contains(tool_call_id)
|
||||
&& seen_ids.insert(tool_call_id)
|
||||
{
|
||||
tool_messages.push(messages[next].clone());
|
||||
}
|
||||
next += 1;
|
||||
}
|
||||
|
||||
if seen_ids.len() == expected_ids.len() {
|
||||
sanitized.push(message.clone());
|
||||
sanitized.extend(tool_messages);
|
||||
} else {
|
||||
let content = message.content.trim();
|
||||
if !content.is_empty() {
|
||||
let mut assistant = message.clone();
|
||||
assistant.tool_calls = None;
|
||||
sanitized.push(assistant);
|
||||
}
|
||||
eprintln!(
|
||||
"跳过不完整 assistant tool_calls 历史片段: expected={}, actual={}",
|
||||
expected_ids.len(),
|
||||
seen_ids.len()
|
||||
);
|
||||
}
|
||||
|
||||
index = next;
|
||||
continue;
|
||||
}
|
||||
|
||||
if message.role == "tool" {
|
||||
eprintln!("跳过没有对应 assistant tool_calls 的历史 tool 消息");
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
sanitized.push(message.clone());
|
||||
index += 1;
|
||||
}
|
||||
|
||||
sanitized
|
||||
}
|
||||
|
||||
fn build_session_summary(
|
||||
session: &ContextSession,
|
||||
messages: &[ContextMessageRow],
|
||||
@@ -731,7 +803,16 @@ fn read_i64_env(name: &str, default: i64) -> i64 {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::detect_assistant_name_preference;
|
||||
use super::{detect_assistant_name_preference, sanitize_tool_call_messages};
|
||||
use ias_common::model::ai::{ChatCompletionRequestMessage, ChatCompletionToolCall};
|
||||
|
||||
fn tool_call(id: &str) -> ChatCompletionToolCall {
|
||||
ChatCompletionToolCall {
|
||||
function: None,
|
||||
id: id.to_string(),
|
||||
call_type: "function".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_assistant_name_preference() {
|
||||
@@ -754,4 +835,53 @@ mod tests {
|
||||
assert_eq!(detect_assistant_name_preference("不要叫知微"), None);
|
||||
assert_eq!(detect_assistant_name_preference("你是谁?"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_complete_multi_tool_call_sequence() {
|
||||
let messages = sanitize_tool_call_messages(vec![
|
||||
ChatCompletionRequestMessage::new("user".to_string(), "查一下".to_string()),
|
||||
ChatCompletionRequestMessage::assistant(
|
||||
"assistant".to_string(),
|
||||
String::new(),
|
||||
Some(vec![tool_call("call_1"), tool_call("call_2")]),
|
||||
),
|
||||
ChatCompletionRequestMessage::tool(
|
||||
"tool".to_string(),
|
||||
"结果 1".to_string(),
|
||||
"call_1".to_string(),
|
||||
),
|
||||
ChatCompletionRequestMessage::tool(
|
||||
"tool".to_string(),
|
||||
"结果 2".to_string(),
|
||||
"call_2".to_string(),
|
||||
),
|
||||
]);
|
||||
|
||||
assert_eq!(messages.len(), 4);
|
||||
assert_eq!(messages[1].tool_calls.as_ref().unwrap().len(), 2);
|
||||
assert_eq!(messages[2].tool_call_id.as_deref(), Some("call_1"));
|
||||
assert_eq!(messages[3].tool_call_id.as_deref(), Some("call_2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drops_incomplete_tool_call_sequence() {
|
||||
let messages = sanitize_tool_call_messages(vec![
|
||||
ChatCompletionRequestMessage::new("user".to_string(), "查一下".to_string()),
|
||||
ChatCompletionRequestMessage::assistant(
|
||||
"assistant".to_string(),
|
||||
String::new(),
|
||||
Some(vec![tool_call("call_1"), tool_call("call_2")]),
|
||||
),
|
||||
ChatCompletionRequestMessage::tool(
|
||||
"tool".to_string(),
|
||||
"结果 1".to_string(),
|
||||
"call_1".to_string(),
|
||||
),
|
||||
ChatCompletionRequestMessage::new("user".to_string(), "继续".to_string()),
|
||||
]);
|
||||
|
||||
assert_eq!(messages.len(), 2);
|
||||
assert_eq!(messages[0].role, "user");
|
||||
assert_eq!(messages[1].content, "继续");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user