Files
windmill/backend/windmill-common/src/flow_conversations.rs
T
Ruben Fiszelandwindmill-internal-app[bot] faa2aaf214 fix: truncate strings on char boundaries to avoid panics on multibyte input (#10390)
* fix: truncate strings on char boundaries to avoid panics on multibyte input

* fix: add borrowed truncate_chars helper and pin ee ref for audit fix

* docs: clarify truncate_with_ellipsis length contract

* chore: update ee-repo-ref to bbfb0de0dc9fa06130a231eb10c64f60238d1bbd

This commit updates the EE repository reference after PR #690 was merged in windmill-ee-private.

Previous ee-repo-ref: de15aeff12daf457711f5b981de691418484535c

New ee-repo-ref: bbfb0de0dc9fa06130a231eb10c64f60238d1bbd

Automated by sync-ee-ref workflow.

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-07-28 18:29:45 +02:00

146 lines
3.9 KiB
Rust

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{self, FromRow};
use uuid::Uuid;
use crate::db::DB;
use crate::error::Result;
use crate::utils::truncate_with_ellipsis;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
#[sqlx(type_name = "MESSAGE_TYPE", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum MessageType {
User,
Assistant,
System,
Tool,
}
#[derive(Serialize, FromRow, Debug)]
pub struct FlowConversation {
pub id: Uuid,
pub workspace_id: String,
pub flow_path: String,
pub title: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub created_by: String,
}
pub async fn get_or_create_conversation_with_id(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
w_id: &str,
flow_path: &str,
username: &str,
title: &str,
conversation_id: Uuid,
) -> Result<FlowConversation> {
// Check if conversation already exists
let existing_conversation = sqlx::query_as!(
FlowConversation,
"SELECT id, workspace_id, flow_path, title, created_at, updated_at, created_by
FROM flow_conversation
WHERE id = $1 AND workspace_id = $2",
conversation_id,
w_id
)
.fetch_optional(&mut **tx)
.await?;
if let Some(existing) = existing_conversation {
return Ok(existing);
}
// Truncate title to 25 characters max
let title = truncate_with_ellipsis(title, 25);
// Create new conversation with provided ID
let conversation = sqlx::query_as!(
FlowConversation,
"INSERT INTO flow_conversation (id, workspace_id, flow_path, created_by, title)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, workspace_id, flow_path, title, created_at, updated_at, created_by",
conversation_id,
w_id,
flow_path,
username,
title
)
.fetch_one(&mut **tx)
.await?;
Ok(conversation)
}
/// Add a message to a conversation using an existing transaction
/// If the conversation doesn't exist, logs a warning and returns Ok (no error thrown)
/// This allows memory_id to be used for agent memory without requiring a conversation
pub async fn add_message_to_conversation_tx(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
conversation_id: Uuid,
job_id: Option<Uuid>,
content: &str,
message_type: MessageType,
step_name: Option<&str>,
success: bool,
) -> Result<()> {
// Check if conversation exists first
let conversation_exists = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM flow_conversation WHERE id = $1) as \"exists!\"",
conversation_id
)
.fetch_one(&mut **tx)
.await?
.exists;
if !conversation_exists {
tracing::warn!(
"Conversation {} does not exist. Skipping message insertion. This is expected when flows are called from apps (memory_id is used for agent memory only).",
conversation_id
);
return Ok(());
}
// Insert the message
sqlx::query!(
"INSERT INTO flow_conversation_message (conversation_id, message_type, content, job_id, step_name, success)
VALUES ($1, $2, $3, $4, $5, $6)",
conversation_id,
message_type as MessageType,
content,
job_id,
step_name,
success
)
.execute(&mut **tx)
.await?;
// Update conversation updated_at timestamp
sqlx::query!(
"UPDATE flow_conversation SET updated_at = NOW() WHERE id = $1",
conversation_id
)
.execute(&mut **tx)
.await?;
Ok(())
}
/// Delete all memory for a conversation from the database
pub async fn delete_conversation_memory(
db: &DB,
workspace_id: &str,
conversation_id: Uuid,
) -> Result<()> {
sqlx::query!(
"DELETE FROM ai_agent_memory WHERE workspace_id = $1 AND conversation_id = $2",
workspace_id,
conversation_id
)
.execute(db)
.await?;
Ok(())
}