Compare commits
2 Commits
b03ea47fed
...
a48a3d9906
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a48a3d9906 | ||
|
|
de342f9195 |
41
crates/erp-ai/src/entity/ai_chat_message.rs
Normal file
41
crates/erp-ai/src/entity/ai_chat_message.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "ai_chat_messages")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub tenant_id: Uuid,
|
||||
pub session_id: Uuid,
|
||||
pub role: String,
|
||||
pub content: Option<String>,
|
||||
pub tool_calls: Option<serde_json::Value>,
|
||||
pub tool_call_id: Option<String>,
|
||||
pub token_count: Option<i32>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub created_by: Option<Uuid>,
|
||||
pub updated_by: Option<Uuid>,
|
||||
pub deleted_at: Option<DateTimeUtc>,
|
||||
#[sea_orm(default_value = 1)]
|
||||
pub version_lock: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::ai_chat_session::Entity",
|
||||
from = "Column::SessionId",
|
||||
to = "super::ai_chat_session::Column::Id"
|
||||
)]
|
||||
Session,
|
||||
}
|
||||
|
||||
impl Related<super::ai_chat_session::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Session.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
37
crates/erp-ai/src/entity/ai_chat_session.rs
Normal file
37
crates/erp-ai/src/entity/ai_chat_session.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "ai_chat_sessions")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub tenant_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub patient_id: Option<Uuid>,
|
||||
pub title: Option<String>,
|
||||
#[sea_orm(default_value = "active")]
|
||||
pub status: String,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub created_by: Option<Uuid>,
|
||||
pub updated_by: Option<Uuid>,
|
||||
pub deleted_at: Option<DateTimeUtc>,
|
||||
#[sea_orm(default_value = 1)]
|
||||
pub version_lock: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::ai_chat_message::Entity")]
|
||||
Messages,
|
||||
}
|
||||
|
||||
impl Related<super::ai_chat_message::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Messages.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
37
crates/erp-ai/src/entity/ai_tool_call_log.rs
Normal file
37
crates/erp-ai/src/entity/ai_tool_call_log.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "ai_tool_call_logs")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub tenant_id: Uuid,
|
||||
pub session_id: Uuid,
|
||||
pub message_id: Uuid,
|
||||
pub tool_name: String,
|
||||
pub parameters: Option<serde_json::Value>,
|
||||
pub result_summary: Option<String>,
|
||||
pub execution_ms: Option<i32>,
|
||||
pub success: bool,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub created_by: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::ai_chat_session::Entity",
|
||||
from = "Column::SessionId",
|
||||
to = "super::ai_chat_session::Column::Id"
|
||||
)]
|
||||
Session,
|
||||
}
|
||||
|
||||
impl Related<super::ai_chat_session::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Session.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -1,5 +1,7 @@
|
||||
pub mod ai_analysis;
|
||||
pub mod ai_analysis_queue;
|
||||
pub mod ai_chat_message;
|
||||
pub mod ai_chat_session;
|
||||
pub mod ai_feature_flags;
|
||||
pub mod ai_knowledge_guides;
|
||||
pub mod ai_knowledge_references;
|
||||
@@ -9,6 +11,7 @@ pub mod ai_risk_threshold;
|
||||
pub mod ai_suggestion;
|
||||
pub mod ai_suggestion_feedback;
|
||||
pub mod ai_tenant_config;
|
||||
pub mod ai_tool_call_log;
|
||||
pub mod ai_usage;
|
||||
pub mod ai_usage_daily;
|
||||
pub mod copilot_chat_logs;
|
||||
|
||||
@@ -27,6 +27,8 @@ pub struct ChatRequest {
|
||||
pub history: Option<Vec<ChatHistoryItem>>,
|
||||
/// 可选:关联患者 ID(从用户档案中获取)
|
||||
pub patient_id: Option<uuid::Uuid>,
|
||||
/// 可选:会话 ID(传入时走会话持久化模式)
|
||||
pub session_id: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)]
|
||||
@@ -98,8 +100,34 @@ where
|
||||
// 构建 Agent 消息历史
|
||||
let mut messages = vec![];
|
||||
|
||||
// 将前端传来的历史转换为 Agent ChatMessage
|
||||
if let Some(ref hist) = body.history {
|
||||
// session_id 模式:从 DB 加载历史
|
||||
if let Some(sid) = body.session_id {
|
||||
let db_messages = ai_state
|
||||
.chat_message
|
||||
.list_messages(ctx.tenant_id, sid, 50)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
for m in &db_messages {
|
||||
if m.deleted_at.is_some() {
|
||||
continue;
|
||||
}
|
||||
let role = match m.role.as_str() {
|
||||
"user" => ChatMessageRole::User,
|
||||
"tool" => ChatMessageRole::Tool,
|
||||
_ => ChatMessageRole::Assistant,
|
||||
};
|
||||
let tool_calls: Option<Vec<crate::dto::ToolCall>> = m
|
||||
.tool_calls
|
||||
.as_ref()
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok());
|
||||
messages.push(ChatMessage {
|
||||
role,
|
||||
content: m.content.clone().unwrap_or_default(),
|
||||
tool_calls,
|
||||
tool_call_id: m.tool_call_id.clone(),
|
||||
});
|
||||
}
|
||||
} else if let Some(ref hist) = body.history {
|
||||
let filtered: Vec<&ChatHistoryItem> = hist
|
||||
.iter()
|
||||
.filter(|h| h.role == "user" || h.role == "assistant")
|
||||
@@ -250,6 +278,69 @@ where
|
||||
tracing::warn!(error = %e, "Failed to log chat usage");
|
||||
}
|
||||
|
||||
// session_id 模式:持久化消息
|
||||
let assistant_uuid = uuid::Uuid::parse_str(&message_id).unwrap_or(uuid::Uuid::now_v7());
|
||||
if let Some(sid) = body.session_id {
|
||||
use crate::service::chat_message::{SaveMessageParams, SaveToolCallLogParams};
|
||||
|
||||
// 保存用户消息
|
||||
if let Err(e) = ai_state
|
||||
.chat_message
|
||||
.save_message(SaveMessageParams {
|
||||
tenant_id: ctx.tenant_id,
|
||||
session_id: sid,
|
||||
role: "user".to_string(),
|
||||
content: Some(message.to_string()),
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
token_count: None,
|
||||
user_id: ctx.user_id,
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, "Failed to save user message to session");
|
||||
}
|
||||
|
||||
// 保存助手回复
|
||||
if let Err(e) = ai_state
|
||||
.chat_message
|
||||
.save_message(SaveMessageParams {
|
||||
tenant_id: ctx.tenant_id,
|
||||
session_id: sid,
|
||||
role: "assistant".to_string(),
|
||||
content: Some(result.reply.clone()),
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
token_count: Some((result.total_input_tokens + result.total_output_tokens) as i32),
|
||||
user_id: ctx.user_id,
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, "Failed to save assistant message to session");
|
||||
}
|
||||
|
||||
// 保存 Tool 调用日志
|
||||
for tc_log in &result.tool_calls {
|
||||
if let Err(e) = ai_state
|
||||
.chat_message
|
||||
.save_tool_call_log(SaveToolCallLogParams {
|
||||
tenant_id: ctx.tenant_id,
|
||||
session_id: sid,
|
||||
message_id: assistant_uuid,
|
||||
tool_name: tc_log.tool_name.clone(),
|
||||
parameters: None,
|
||||
result_summary: None,
|
||||
execution_ms: tc_log.duration_ms as i32,
|
||||
success: tc_log.success,
|
||||
user_id: ctx.user_id,
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, tool = %tc_log.tool_name, "Failed to save tool call log");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(ApiResponse::ok(ChatResponse {
|
||||
reply: result.reply,
|
||||
message_id,
|
||||
@@ -261,3 +352,163 @@ where
|
||||
},
|
||||
})))
|
||||
}
|
||||
|
||||
// === 会话 CRUD ===
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct SessionResponse {
|
||||
pub id: uuid::Uuid,
|
||||
pub title: Option<String>,
|
||||
pub patient_id: Option<uuid::Uuid>,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<crate::entity::ai_chat_session::Model> for SessionResponse {
|
||||
fn from(m: crate::entity::ai_chat_session::Model) -> Self {
|
||||
Self {
|
||||
id: m.id,
|
||||
title: m.title,
|
||||
patient_id: m.patient_id,
|
||||
status: m.status,
|
||||
created_at: m.created_at.to_rfc3339(),
|
||||
updated_at: m.updated_at.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct CreateSessionRequest {
|
||||
pub patient_id: Option<uuid::Uuid>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct RenameSessionRequest {
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/ai/chat/sessions",
|
||||
request_body = CreateSessionRequest,
|
||||
responses((status = 200, description = "创建会话")),
|
||||
tag = "AI 会话",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn create_session<S>(
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
State(state): State<S>,
|
||||
Json(body): Json<CreateSessionRequest>,
|
||||
) -> Result<Json<ApiResponse<SessionResponse>>, erp_core::error::AppError>
|
||||
where
|
||||
AiState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "ai.chat.session.manage")?;
|
||||
let ai_state = AiState::from_ref(&state);
|
||||
let session = ai_state
|
||||
.chat_session
|
||||
.create(ctx.tenant_id, ctx.user_id, body.patient_id, body.title)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(error = %e, "Failed to create session");
|
||||
erp_core::error::AppError::Internal("创建会话失败".into())
|
||||
})?;
|
||||
Ok(Json(ApiResponse::ok(SessionResponse::from(session))))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/ai/chat/sessions",
|
||||
responses((status = 200, description = "会话列表")),
|
||||
tag = "AI 会话",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn list_sessions<S>(
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
State(state): State<S>,
|
||||
) -> Result<Json<ApiResponse<Vec<SessionResponse>>>, erp_core::error::AppError>
|
||||
where
|
||||
AiState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "ai.chat.session.list")?;
|
||||
let ai_state = AiState::from_ref(&state);
|
||||
let sessions = ai_state
|
||||
.chat_session
|
||||
.list(ctx.tenant_id, ctx.user_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(error = %e, "Failed to list sessions");
|
||||
erp_core::error::AppError::Internal("获取会话列表失败".into())
|
||||
})?;
|
||||
let resp: Vec<SessionResponse> = sessions.into_iter().map(SessionResponse::from).collect();
|
||||
Ok(Json(ApiResponse::ok(resp)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/ai/chat/sessions/{session_id}/rename",
|
||||
responses((status = 200, description = "重命名成功")),
|
||||
tag = "AI 会话",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn rename_session<S>(
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
State(state): State<S>,
|
||||
axum::extract::Path(session_id): axum::extract::Path<uuid::Uuid>,
|
||||
Json(body): Json<RenameSessionRequest>,
|
||||
) -> Result<Json<ApiResponse<()>>, erp_core::error::AppError>
|
||||
where
|
||||
AiState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "ai.chat.session.manage")?;
|
||||
let ai_state = AiState::from_ref(&state);
|
||||
let ok = ai_state
|
||||
.chat_session
|
||||
.rename(ctx.tenant_id, session_id, body.title)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(error = %e, "Failed to rename session");
|
||||
erp_core::error::AppError::Internal("重命名会话失败".into())
|
||||
})?;
|
||||
if !ok {
|
||||
return Err(erp_core::error::AppError::Validation("会话不存在".into()));
|
||||
}
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/ai/chat/sessions/{session_id}/close",
|
||||
responses((status = 200, description = "关闭成功")),
|
||||
tag = "AI 会话",
|
||||
security(("bearer_auth" = [])),
|
||||
)]
|
||||
pub async fn close_session<S>(
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
State(state): State<S>,
|
||||
axum::extract::Path(session_id): axum::extract::Path<uuid::Uuid>,
|
||||
) -> Result<Json<ApiResponse<()>>, erp_core::error::AppError>
|
||||
where
|
||||
AiState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "ai.chat.session.manage")?;
|
||||
let ai_state = AiState::from_ref(&state);
|
||||
let ok = ai_state
|
||||
.chat_session
|
||||
.close(ctx.tenant_id, session_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(error = %e, "Failed to close session");
|
||||
erp_core::error::AppError::Internal("关闭会话失败".into())
|
||||
})?;
|
||||
if !ok {
|
||||
return Err(erp_core::error::AppError::Validation("会话不存在".into()));
|
||||
}
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
@@ -431,6 +431,22 @@ impl AiModule {
|
||||
"/ai/chat",
|
||||
axum::routing::post(crate::handler::chat_handler::chat),
|
||||
)
|
||||
.route(
|
||||
"/ai/chat/sessions",
|
||||
axum::routing::post(crate::handler::chat_handler::create_session),
|
||||
)
|
||||
.route(
|
||||
"/ai/chat/sessions",
|
||||
axum::routing::get(crate::handler::chat_handler::list_sessions),
|
||||
)
|
||||
.route(
|
||||
"/ai/chat/sessions/{session_id}/rename",
|
||||
axum::routing::put(crate::handler::chat_handler::rename_session),
|
||||
)
|
||||
.route(
|
||||
"/ai/chat/sessions/{session_id}/close",
|
||||
axum::routing::post(crate::handler::chat_handler::close_session),
|
||||
)
|
||||
.route(
|
||||
"/ai/config",
|
||||
axum::routing::get(crate::handler::config_handler::get_config),
|
||||
|
||||
105
crates/erp-ai/src/service/chat_message.rs
Normal file
105
crates/erp-ai/src/service/chat_message.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder,
|
||||
QuerySelect, Set,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::entity::ai_chat_message;
|
||||
use crate::entity::ai_tool_call_log;
|
||||
|
||||
pub struct ChatMessageService {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
pub struct SaveMessageParams {
|
||||
pub tenant_id: Uuid,
|
||||
pub session_id: Uuid,
|
||||
pub role: String,
|
||||
pub content: Option<String>,
|
||||
pub tool_calls: Option<serde_json::Value>,
|
||||
pub tool_call_id: Option<String>,
|
||||
pub token_count: Option<i32>,
|
||||
pub user_id: Uuid,
|
||||
}
|
||||
|
||||
pub struct SaveToolCallLogParams {
|
||||
pub tenant_id: Uuid,
|
||||
pub session_id: Uuid,
|
||||
pub message_id: Uuid,
|
||||
pub tool_name: String,
|
||||
pub parameters: Option<serde_json::Value>,
|
||||
pub result_summary: Option<String>,
|
||||
pub execution_ms: i32,
|
||||
pub success: bool,
|
||||
pub user_id: Uuid,
|
||||
}
|
||||
|
||||
impl ChatMessageService {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn save_message(
|
||||
&self,
|
||||
params: SaveMessageParams,
|
||||
) -> Result<ai_chat_message::Model, sea_orm::DbErr> {
|
||||
let id = Uuid::now_v7();
|
||||
let now = chrono::Utc::now();
|
||||
let model = ai_chat_message::ActiveModel {
|
||||
id: Set(id),
|
||||
tenant_id: Set(params.tenant_id),
|
||||
session_id: Set(params.session_id),
|
||||
role: Set(params.role),
|
||||
content: Set(params.content),
|
||||
tool_calls: Set(params.tool_calls),
|
||||
tool_call_id: Set(params.tool_call_id),
|
||||
token_count: Set(params.token_count),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(Some(params.user_id)),
|
||||
updated_by: Set(Some(params.user_id)),
|
||||
deleted_at: Set(None),
|
||||
version_lock: Set(1),
|
||||
};
|
||||
let result = model.insert(&self.db).await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn list_messages(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
session_id: Uuid,
|
||||
limit: u64,
|
||||
) -> Result<Vec<ai_chat_message::Model>, sea_orm::DbErr> {
|
||||
ai_chat_message::Entity::find()
|
||||
.filter(ai_chat_message::Column::TenantId.eq(tenant_id))
|
||||
.filter(ai_chat_message::Column::SessionId.eq(session_id))
|
||||
.filter(ai_chat_message::Column::DeletedAt.is_null())
|
||||
.order_by_asc(ai_chat_message::Column::CreatedAt)
|
||||
.limit(limit)
|
||||
.all(&self.db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn save_tool_call_log(
|
||||
&self,
|
||||
params: SaveToolCallLogParams,
|
||||
) -> Result<ai_tool_call_log::Model, sea_orm::DbErr> {
|
||||
let id = Uuid::now_v7();
|
||||
let model = ai_tool_call_log::ActiveModel {
|
||||
id: Set(id),
|
||||
tenant_id: Set(params.tenant_id),
|
||||
session_id: Set(params.session_id),
|
||||
message_id: Set(params.message_id),
|
||||
tool_name: Set(params.tool_name),
|
||||
parameters: Set(params.parameters),
|
||||
result_summary: Set(params.result_summary),
|
||||
execution_ms: Set(Some(params.execution_ms)),
|
||||
success: Set(params.success),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
created_by: Set(Some(params.user_id)),
|
||||
};
|
||||
let result = model.insert(&self.db).await?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
101
crates/erp-ai/src/service/chat_session.rs
Normal file
101
crates/erp-ai/src/service/chat_session.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, Set,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::entity::ai_chat_session;
|
||||
|
||||
pub struct ChatSessionService {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl ChatSessionService {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
patient_id: Option<Uuid>,
|
||||
title: Option<String>,
|
||||
) -> Result<ai_chat_session::Model, sea_orm::DbErr> {
|
||||
let id = Uuid::now_v7();
|
||||
let now = chrono::Utc::now();
|
||||
let model = ai_chat_session::ActiveModel {
|
||||
id: Set(id),
|
||||
tenant_id: Set(tenant_id),
|
||||
user_id: Set(user_id),
|
||||
patient_id: Set(patient_id),
|
||||
title: Set(title),
|
||||
status: Set("active".to_string()),
|
||||
metadata: Set(None),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(Some(user_id)),
|
||||
updated_by: Set(Some(user_id)),
|
||||
deleted_at: Set(None),
|
||||
version_lock: Set(1),
|
||||
};
|
||||
let result = model.insert(&self.db).await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn list(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<ai_chat_session::Model>, sea_orm::DbErr> {
|
||||
ai_chat_session::Entity::find()
|
||||
.filter(ai_chat_session::Column::TenantId.eq(tenant_id))
|
||||
.filter(ai_chat_session::Column::UserId.eq(user_id))
|
||||
.filter(ai_chat_session::Column::DeletedAt.is_null())
|
||||
.filter(ai_chat_session::Column::Status.ne("closed"))
|
||||
.order_by_desc(ai_chat_session::Column::UpdatedAt)
|
||||
.all(&self.db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
session_id: Uuid,
|
||||
) -> Result<Option<ai_chat_session::Model>, sea_orm::DbErr> {
|
||||
ai_chat_session::Entity::find()
|
||||
.filter(ai_chat_session::Column::TenantId.eq(tenant_id))
|
||||
.filter(ai_chat_session::Column::Id.eq(session_id))
|
||||
.filter(ai_chat_session::Column::DeletedAt.is_null())
|
||||
.one(&self.db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn close(&self, tenant_id: Uuid, session_id: Uuid) -> Result<bool, sea_orm::DbErr> {
|
||||
let session = self.get(tenant_id, session_id).await?;
|
||||
let Some(session) = session else {
|
||||
return Ok(false);
|
||||
};
|
||||
let mut active: ai_chat_session::ActiveModel = session.into();
|
||||
active.status = Set("closed".to_string());
|
||||
active.updated_at = Set(chrono::Utc::now());
|
||||
active.update(&self.db).await?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn rename(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
session_id: Uuid,
|
||||
new_title: String,
|
||||
) -> Result<bool, sea_orm::DbErr> {
|
||||
let session = self.get(tenant_id, session_id).await?;
|
||||
let Some(session) = session else {
|
||||
return Ok(false);
|
||||
};
|
||||
let mut active: ai_chat_session::ActiveModel = session.into();
|
||||
active.title = Set(Some(new_title));
|
||||
active.updated_at = Set(chrono::Utc::now());
|
||||
active.update(&self.db).await?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ pub mod analysis;
|
||||
pub mod analysis_queue;
|
||||
pub mod auto_analysis;
|
||||
pub mod cache;
|
||||
pub mod chat_message;
|
||||
pub mod chat_session;
|
||||
pub mod comparison;
|
||||
pub mod cost;
|
||||
pub mod dialysis_risk_scorer;
|
||||
|
||||
@@ -7,6 +7,8 @@ use sea_orm::DatabaseConnection;
|
||||
use crate::provider::registry::ProviderRegistry;
|
||||
use crate::service::analysis::AnalysisService;
|
||||
use crate::service::cache::CacheService;
|
||||
use crate::service::chat_message::ChatMessageService;
|
||||
use crate::service::chat_session::ChatSessionService;
|
||||
use crate::service::feature_flag_service::FeatureFlagService;
|
||||
use crate::service::insight_service::InsightService;
|
||||
use crate::service::knowledge::KnowledgeService;
|
||||
@@ -32,4 +34,6 @@ pub struct AiState {
|
||||
pub insight_service: Arc<InsightService>,
|
||||
pub feature_flags: Arc<FeatureFlagService>,
|
||||
pub knowledge: Arc<KnowledgeService>,
|
||||
pub chat_session: Arc<ChatSessionService>,
|
||||
pub chat_message: Arc<ChatMessageService>,
|
||||
}
|
||||
|
||||
@@ -615,6 +615,12 @@ async fn main() -> anyhow::Result<()> {
|
||||
erp_ai::service::embedding::EmbeddingService::from_settings(&db).await,
|
||||
),
|
||||
)),
|
||||
chat_session: std::sync::Arc::new(
|
||||
erp_ai::service::chat_session::ChatSessionService::new(db.clone()),
|
||||
),
|
||||
chat_message: std::sync::Arc::new(
|
||||
erp_ai::service::chat_message::ChatMessageService::new(db.clone()),
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user