基于promptx/outfit-match中的query_llm_with_grounding实现,遵循Tauri开发规范 ## 新增功能 - 实现RAG Grounding核心服务逻辑 - 支持基于Vertex AI Search的检索增强生成 - 集成Cloudflare Gateway和Google Gemini API - 提供完整的TypeScript类型定义和前端服务封装 ## 技术架构 - Rust后端:GeminiService扩展RAG功能 - Tauri命令:query_rag_grounding, test_rag_grounding_connection, get_rag_grounding_config - 前端服务:RagGroundingService类,支持统计监控和错误处理 - 数据模型:完整的请求/响应结构和配置管理 ## 核心特性 - 检索增强生成:基于数据存储的智能检索和内容生成 - 容错机制:内置重试机制和错误处理 - 性能监控:响应时间统计和性能指标 - 类型安全:完整的TypeScript类型定义 - 会话管理:支持上下文保持的对话功能 ## 测试覆盖 - 25个单元测试,覆盖核心功能和边界情况 - 包括序列化/反序列化、错误处理、配置验证等 - 所有测试通过,确保功能正确性 ## 文档和示例 - 完整的API文档 (docs/rag-grounding-api.md) - 详细的使用示例 (examples/rag-grounding-usage.ts) - 包含最佳实践和错误处理指南 ## 遵循规范 - 严格遵循promptx/tauri-desktop-app-expert开发规范 - 安全第一:最小权限原则,数据加密保护 - 性能优先:异步处理,响应时间优化 - 模块化设计:清晰的架构分层 - 错误处理完善:全面的错误处理和用户反馈
227 lines
8.0 KiB
Rust
227 lines
8.0 KiB
Rust
#[cfg(test)]
|
||
mod rag_grounding_tests {
|
||
use crate::infrastructure::gemini_service::{
|
||
GeminiService, GeminiConfig, RagGroundingRequest, RagGroundingConfig, RagGroundingResponse,
|
||
GroundingMetadata, GroundingSource,
|
||
};
|
||
|
||
/// 创建测试用的 GeminiService 实例
|
||
fn create_test_service() -> GeminiService {
|
||
let config = GeminiConfig::default();
|
||
GeminiService::new(Some(config)).expect("Failed to create test service")
|
||
}
|
||
|
||
/// 创建测试用的 RAG Grounding 请求
|
||
fn create_test_request() -> RagGroundingRequest {
|
||
RagGroundingRequest {
|
||
user_input: "测试查询".to_string(),
|
||
config: Some(RagGroundingConfig::default()),
|
||
session_id: Some("test-session".to_string()),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_rag_grounding_config_default() {
|
||
let config = RagGroundingConfig::default();
|
||
|
||
assert_eq!(config.project_id, "gen-lang-client-0413414134");
|
||
assert_eq!(config.location, "global");
|
||
assert_eq!(config.data_store_id, "default_data_store");
|
||
assert_eq!(config.model_id, "gemini-2.5-flash");
|
||
assert_eq!(config.temperature, 1.0);
|
||
assert_eq!(config.max_output_tokens, 8192);
|
||
assert!(config.system_prompt.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn test_rag_grounding_request_creation() {
|
||
let request = create_test_request();
|
||
|
||
assert_eq!(request.user_input, "测试查询");
|
||
assert!(request.config.is_some());
|
||
assert_eq!(request.session_id, Some("test-session".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn test_rag_grounding_request_serialization() {
|
||
let request = create_test_request();
|
||
|
||
// 测试序列化
|
||
let serialized = serde_json::to_string(&request).expect("Failed to serialize request");
|
||
assert!(serialized.contains("测试查询"));
|
||
assert!(serialized.contains("test-session"));
|
||
|
||
// 测试反序列化
|
||
let deserialized: RagGroundingRequest = serde_json::from_str(&serialized)
|
||
.expect("Failed to deserialize request");
|
||
assert_eq!(deserialized.user_input, request.user_input);
|
||
assert_eq!(deserialized.session_id, request.session_id);
|
||
}
|
||
|
||
#[test]
|
||
fn test_rag_grounding_response_creation() {
|
||
let response = RagGroundingResponse {
|
||
answer: "测试回答".to_string(),
|
||
grounding_metadata: Some(GroundingMetadata {
|
||
sources: vec![GroundingSource {
|
||
title: "测试来源".to_string(),
|
||
uri: Some("https://example.com".to_string()),
|
||
snippet: "测试片段".to_string(),
|
||
relevance_score: Some(0.95),
|
||
}],
|
||
search_queries: vec!["测试查询".to_string()],
|
||
}),
|
||
response_time_ms: 1500,
|
||
model_used: "gemini-2.5-flash".to_string(),
|
||
};
|
||
|
||
assert_eq!(response.answer, "测试回答");
|
||
assert!(response.grounding_metadata.is_some());
|
||
assert_eq!(response.response_time_ms, 1500);
|
||
assert_eq!(response.model_used, "gemini-2.5-flash");
|
||
|
||
let metadata = response.grounding_metadata.unwrap();
|
||
assert_eq!(metadata.sources.len(), 1);
|
||
assert_eq!(metadata.search_queries.len(), 1);
|
||
|
||
let source = &metadata.sources[0];
|
||
assert_eq!(source.title, "测试来源");
|
||
assert_eq!(source.uri, Some("https://example.com".to_string()));
|
||
assert_eq!(source.snippet, "测试片段");
|
||
assert_eq!(source.relevance_score, Some(0.95));
|
||
}
|
||
|
||
#[test]
|
||
fn test_grounding_metadata_empty() {
|
||
let metadata = GroundingMetadata {
|
||
sources: vec![],
|
||
search_queries: vec![],
|
||
};
|
||
|
||
assert!(metadata.sources.is_empty());
|
||
assert!(metadata.search_queries.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn test_grounding_source_without_optional_fields() {
|
||
let source = GroundingSource {
|
||
title: "测试来源".to_string(),
|
||
uri: None,
|
||
snippet: "测试片段".to_string(),
|
||
relevance_score: None,
|
||
};
|
||
|
||
assert_eq!(source.title, "测试来源");
|
||
assert!(source.uri.is_none());
|
||
assert_eq!(source.snippet, "测试片段");
|
||
assert!(source.relevance_score.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn test_rag_grounding_config_with_system_prompt() {
|
||
let mut config = RagGroundingConfig::default();
|
||
config.system_prompt = Some("你是一个专业的助手".to_string());
|
||
|
||
assert!(config.system_prompt.is_some());
|
||
assert_eq!(config.system_prompt.unwrap(), "你是一个专业的助手");
|
||
}
|
||
|
||
#[test]
|
||
fn test_rag_grounding_config_custom_values() {
|
||
let config = RagGroundingConfig {
|
||
project_id: "custom-project".to_string(),
|
||
location: "us-central1".to_string(),
|
||
data_store_id: "custom-datastore".to_string(),
|
||
model_id: "gemini-pro".to_string(),
|
||
temperature: 0.5,
|
||
max_output_tokens: 4096,
|
||
system_prompt: Some("自定义系统提示".to_string()),
|
||
};
|
||
|
||
assert_eq!(config.project_id, "custom-project");
|
||
assert_eq!(config.location, "us-central1");
|
||
assert_eq!(config.data_store_id, "custom-datastore");
|
||
assert_eq!(config.model_id, "gemini-pro");
|
||
assert_eq!(config.temperature, 0.5);
|
||
assert_eq!(config.max_output_tokens, 4096);
|
||
assert_eq!(config.system_prompt, Some("自定义系统提示".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn test_gemini_service_creation() {
|
||
let service = create_test_service();
|
||
// 基本的服务创建测试,确保没有panic
|
||
assert!(true); // 如果能到这里说明服务创建成功
|
||
}
|
||
|
||
/// 测试 JSON 响应解析
|
||
#[test]
|
||
fn test_parse_rag_grounding_response_json() {
|
||
let service = create_test_service();
|
||
|
||
// 模拟一个典型的 Gemini API 响应
|
||
let mock_response = r#"{
|
||
"candidates": [{
|
||
"content": {
|
||
"parts": [{
|
||
"text": "这是一个测试回答"
|
||
}]
|
||
},
|
||
"groundingMetadata": {
|
||
"groundingSources": [{
|
||
"title": "测试文档",
|
||
"uri": "https://example.com/doc",
|
||
"snippet": "这是一个测试片段",
|
||
"relevanceScore": 0.85
|
||
}],
|
||
"searchQueries": ["测试查询"]
|
||
}
|
||
}]
|
||
}"#;
|
||
|
||
let result = service.parse_rag_grounding_response(mock_response);
|
||
assert!(result.is_ok());
|
||
|
||
let response = result.unwrap();
|
||
assert_eq!(response.answer, "这是一个测试回答");
|
||
assert!(response.grounding_metadata.is_some());
|
||
|
||
let metadata = response.grounding_metadata.unwrap();
|
||
assert_eq!(metadata.sources.len(), 1);
|
||
assert_eq!(metadata.search_queries.len(), 1);
|
||
|
||
let source = &metadata.sources[0];
|
||
assert_eq!(source.title, "测试文档");
|
||
assert_eq!(source.uri, Some("https://example.com/doc".to_string()));
|
||
assert_eq!(source.snippet, "这是一个测试片段");
|
||
assert_eq!(source.relevance_score, Some(0.85));
|
||
}
|
||
|
||
/// 测试无效 JSON 响应处理
|
||
#[test]
|
||
fn test_parse_invalid_json_response() {
|
||
let service = create_test_service();
|
||
|
||
let invalid_json = "这不是有效的JSON";
|
||
let result = service.parse_rag_grounding_response(invalid_json);
|
||
assert!(result.is_err());
|
||
}
|
||
|
||
/// 测试缺少必要字段的响应
|
||
#[test]
|
||
fn test_parse_incomplete_response() {
|
||
let service = create_test_service();
|
||
|
||
let incomplete_response = r#"{
|
||
"candidates": []
|
||
}"#;
|
||
|
||
let result = service.parse_rag_grounding_response(incomplete_response);
|
||
assert!(result.is_ok());
|
||
|
||
let response = result.unwrap();
|
||
assert_eq!(response.answer, "无法解析响应内容");
|
||
assert!(response.grounding_metadata.is_none());
|
||
}
|
||
}
|