use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
System,
User,
Assistant,
Tool,
}
impl std::fmt::Display for MessageRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MessageRole::System => write!(f, "system"),
MessageRole::User => write!(f, "user"),
MessageRole::Assistant => write!(f, "assistant"),
MessageRole::Tool => write!(f, "tool"),
}
}
}
impl std::str::FromStr for MessageRole {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"system" => Ok(MessageRole::System),
"user" => Ok(MessageRole::User),
"assistant" => Ok(MessageRole::Assistant),
"tool" => Ok(MessageRole::Tool),
_ => Err(format!("Unknown role: {}", s)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum ApiCallStatus {
#[default]
Success,
Failed,
Timeout,
RateLimited,
Cancelled,
}
impl std::fmt::Display for ApiCallStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApiCallStatus::Success => write!(f, "success"),
ApiCallStatus::Failed => write!(f, "failed"),
ApiCallStatus::Timeout => write!(f, "timeout"),
ApiCallStatus::RateLimited => write!(f, "rate_limited"),
ApiCallStatus::Cancelled => write!(f, "cancelled"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageContent {
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCallContent>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_result: Option<ToolResultContent>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
impl MessageContent {
pub fn text(content: impl Into<String>) -> Self {
Self {
text: Some(content.into()),
tool_calls: None,
tool_result: None,
extra: HashMap::new(),
}
}
pub fn tool_calls(calls: Vec<ToolCallContent>) -> Self {
Self {
text: None,
tool_calls: Some(calls),
tool_result: None,
extra: HashMap::new(),
}
}
pub fn tool_result(result: ToolResultContent) -> Self {
Self {
text: None,
tool_calls: None,
tool_result: Some(result),
extra: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallContent {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResultContent {
pub tool_call_id: String,
pub content: String,
#[serde(default)]
pub is_error: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LLMMessage {
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_message_id: Option<Uuid>,
pub chat_session_id: Uuid,
pub agent_id: Uuid,
pub content: MessageContent,
pub role: MessageRole,
pub user_id: Uuid,
pub tenant_id: Uuid,
pub create_time: chrono::DateTime<chrono::Utc>,
pub update_time: chrono::DateTime<chrono::Utc>,
}
impl LLMMessage {
pub fn new(
chat_session_id: Uuid,
agent_id: Uuid,
user_id: Uuid,
tenant_id: Uuid,
role: MessageRole,
content: MessageContent,
) -> Self {
let now = chrono::Utc::now();
Self {
id: Uuid::now_v7(),
parent_message_id: None,
chat_session_id,
agent_id,
content,
role,
user_id,
tenant_id,
create_time: now,
update_time: now,
}
}
pub fn with_parent(mut self, parent_id: Uuid) -> Self {
self.parent_message_id = Some(parent_id);
self
}
pub fn with_tenant(mut self, tenant_id: Uuid) -> Self {
self.tenant_id = tenant_id;
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TokenDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub cached_tokens: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_tokens: Option<i32>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PriceDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub input_price: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_price: Option<f64>,
#[serde(default = "default_currency")]
pub currency: String,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
fn default_currency() -> String {
"USD".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LLMApiCall {
pub id: Uuid,
pub chat_session_id: Uuid,
pub agent_id: Uuid,
pub user_id: Uuid,
pub tenant_id: Uuid,
pub request_message_id: Uuid,
pub response_message_id: Uuid,
pub model_name: String,
pub prompt_tokens: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_tokens_details: Option<TokenDetails>,
pub completion_tokens: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_tokens_details: Option<TokenDetails>,
pub total_tokens: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_price: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price_details: Option<PriceDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
pub latency_ms: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_to_first_token_ms: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tokens_per_second: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub api_response_id: Option<String>,
pub status: ApiCallStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_code: Option<String>,
pub create_time: chrono::DateTime<chrono::Utc>,
pub update_time: chrono::DateTime<chrono::Utc>,
}
impl LLMApiCall {
pub fn success(
chat_session_id: Uuid,
agent_id: Uuid,
user_id: Uuid,
tenant_id: Uuid,
request_message_id: Uuid,
response_message_id: Uuid,
model_name: impl Into<String>,
prompt_tokens: i32,
completion_tokens: i32,
request_time: chrono::DateTime<chrono::Utc>,
response_time: chrono::DateTime<chrono::Utc>,
) -> Self {
let latency_ms = (response_time - request_time).num_milliseconds() as i32;
let tokens_per_second = if latency_ms > 0 {
Some(completion_tokens as f64 / (latency_ms as f64 / 1000.0))
} else {
None
};
Self {
id: Uuid::now_v7(),
chat_session_id,
agent_id,
user_id,
tenant_id,
request_message_id,
response_message_id,
model_name: model_name.into(),
prompt_tokens,
prompt_tokens_details: None,
completion_tokens,
completion_tokens_details: None,
total_tokens: prompt_tokens + completion_tokens,
total_price: None,
price_details: None,
latency_ms: Some(latency_ms),
time_to_first_token_ms: None,
tokens_per_second,
api_response_id: None,
status: ApiCallStatus::Success,
error_message: None,
error_code: None,
create_time: request_time,
update_time: response_time,
}
}
pub fn failed(
chat_session_id: Uuid,
agent_id: Uuid,
user_id: Uuid,
tenant_id: Uuid,
request_message_id: Uuid,
model_name: impl Into<String>,
error_message: impl Into<String>,
error_code: Option<String>,
request_time: chrono::DateTime<chrono::Utc>,
) -> Self {
let now = chrono::Utc::now();
Self {
id: Uuid::now_v7(),
chat_session_id,
agent_id,
user_id,
tenant_id,
request_message_id,
response_message_id: Uuid::nil(),
model_name: model_name.into(),
prompt_tokens: 0,
prompt_tokens_details: None,
completion_tokens: 0,
completion_tokens_details: None,
total_tokens: 0,
total_price: None,
price_details: None,
latency_ms: Some((now - request_time).num_milliseconds() as i32),
time_to_first_token_ms: None,
tokens_per_second: None,
api_response_id: None,
status: ApiCallStatus::Failed,
error_message: Some(error_message.into()),
error_code,
create_time: request_time,
update_time: now,
}
}
pub fn with_api_response_id(mut self, id: impl Into<String>) -> Self {
self.api_response_id = Some(id.into());
self
}
pub fn with_price(mut self, total_price: f64, details: Option<PriceDetails>) -> Self {
self.total_price = Some(total_price);
self.price_details = details;
self
}
pub fn with_time_to_first_token(mut self, ttft_ms: i32) -> Self {
self.time_to_first_token_ms = Some(ttft_ms);
self
}
pub fn with_token_details(
mut self,
prompt_details: Option<TokenDetails>,
completion_details: Option<TokenDetails>,
) -> Self {
self.prompt_tokens_details = prompt_details;
self.completion_tokens_details = completion_details;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatSession {
pub id: Uuid,
pub user_id: Uuid,
pub agent_id: Uuid,
pub tenant_id: Uuid,
pub title: Option<String>,
pub metadata: HashMap<String, serde_json::Value>,
pub create_time: chrono::DateTime<chrono::Utc>,
pub update_time: chrono::DateTime<chrono::Utc>,
}
impl ChatSession {
pub fn new(user_id: Uuid, agent_id: Uuid) -> Self {
let now = chrono::Utc::now();
Self {
id: Uuid::now_v7(),
user_id,
agent_id,
tenant_id: Uuid::nil(), title: None,
metadata: HashMap::new(),
create_time: now,
update_time: now,
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_id(mut self, id: Uuid) -> Self {
self.id = id;
self
}
pub fn with_tenant_id(mut self, tenant_id: Uuid) -> Self {
self.tenant_id = tenant_id;
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
}
#[derive(Debug, Clone, Default)]
pub struct QueryFilter {
pub user_id: Option<Uuid>,
pub session_id: Option<Uuid>,
pub agent_id: Option<Uuid>,
pub start_time: Option<chrono::DateTime<chrono::Utc>>,
pub end_time: Option<chrono::DateTime<chrono::Utc>>,
pub status: Option<ApiCallStatus>,
pub model_name: Option<String>,
pub offset: Option<i64>,
pub limit: Option<i64>,
}
impl QueryFilter {
pub fn new() -> Self {
Self::default()
}
pub fn user(mut self, user_id: Uuid) -> Self {
self.user_id = Some(user_id);
self
}
pub fn session(mut self, session_id: Uuid) -> Self {
self.session_id = Some(session_id);
self
}
pub fn agent(mut self, agent_id: Uuid) -> Self {
self.agent_id = Some(agent_id);
self
}
pub fn time_range(
mut self,
start: chrono::DateTime<chrono::Utc>,
end: chrono::DateTime<chrono::Utc>,
) -> Self {
self.start_time = Some(start);
self.end_time = Some(end);
self
}
pub fn with_status(mut self, status: ApiCallStatus) -> Self {
self.status = Some(status);
self
}
pub fn model(mut self, model_name: impl Into<String>) -> Self {
self.model_name = Some(model_name.into());
self
}
pub fn paginate(mut self, offset: i64, limit: i64) -> Self {
self.offset = Some(offset);
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UsageStatistics {
pub total_calls: i64,
pub success_count: i64,
pub failed_count: i64,
pub total_tokens: i64,
pub total_prompt_tokens: i64,
pub total_completion_tokens: i64,
pub total_cost: Option<f64>,
pub avg_latency_ms: Option<f64>,
pub avg_tokens_per_second: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Provider {
pub id: Uuid,
pub tenant_id: Uuid,
pub provider_name: String,
pub provider_type: String,
pub api_base: String,
pub api_key: String,
pub enabled: bool,
pub create_time: chrono::DateTime<chrono::Utc>,
pub update_time: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Agent {
pub id: Uuid,
pub tenant_id: Uuid,
pub agent_code: String,
pub agent_name: String,
pub agent_order: i32,
pub agent_status: bool,
pub context_limit: Option<i32>,
pub custom_params: Option<serde_json::Value>,
pub max_completion_tokens: Option<i32>,
pub model_name: String,
pub provider_id: Uuid,
pub response_format: Option<String>,
pub system_prompt: String,
pub temperature: Option<f32>,
pub stream: Option<bool>,
pub thinking: Option<serde_json::Value>,
pub create_time: chrono::DateTime<chrono::Utc>,
pub update_time: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone)]
pub struct AgentConfig {
pub provider: Provider,
pub agent: Agent,
}