Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: clone topic and message route #2551

Merged
merged 3 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions clients/ts-sdk/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -5406,6 +5406,67 @@
]
}
},
"/api/topic/clone": {
"post": {
"tags": [
"Topic"
],
"summary": "Clone Topic",
"description": "Create a new chat topic from a `topic_id`. The new topic will be attched to the owner_id and act as a coordinator for conversation message history of gen-AI chat sessions. Auth'ed user or api key must have an admin or owner role for the specified dataset's organization.",
"operationId": "clone_topic",
"parameters": [
{
"name": "TR-Dataset",
"in": "header",
"description": "The dataset id or tracking_id to use for the request. We assume you intend to use an id if the value is a valid uuid.",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "JSON request payload to create chat topic",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CloneTopicReqPayload"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "The JSON response payload containing the created topic",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Topic"
}
}
}
},
"400": {
"description": "Topic name empty or a service error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponseBody"
}
}
}
}
},
"security": [
{
"ApiKey": [
"admin"
]
}
]
}
},
"/api/topic/owner/{owner_id}": {
"get": {
"tags": [
Expand Down Expand Up @@ -7048,6 +7109,29 @@
}
}
},
"CloneTopicReqPayload": {
"type": "object",
"required": [
"topic_id",
"owner_id"
],
"properties": {
"name": {
"type": "string",
"description": "The name of the topic. If this is not provided, the topic name is the same as the previous topic",
"nullable": true
},
"owner_id": {
"type": "string",
"description": "The owner_id of the topic. This is typically a browser fingerprint or your user's id. It is used to group topics together for a user."
},
"topic_id": {
"type": "string",
"format": "uuid",
"description": "The topic_id to clone from"
}
}
},
"ClusterAnalytics": {
"oneOf": [
{
Expand Down
30 changes: 30 additions & 0 deletions clients/ts-sdk/src/functions/topic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
DeleteTopicData2,
GetAllTopicsForOwnerIdData,
UpdateTopicReqPayload,
CloneTopicReqPayload
} from "../../fetch-client";
import { TrieveSDK } from "../../sdk";

Expand Down Expand Up @@ -40,6 +41,35 @@ export async function createTopic(
);
}

/**
* Clone a chat topic and all its messages to a new topic. Topics are attached to a owner_id’s and act as a coordinator for conversation message history of gen-AI chat sessions. Auth’ed user or api key must have an admin or owner role for the specified dataset’s organization.
*
* Example:
* ```js
*const data = await trieve.cloneTopic({
first_user_message: "hello",
name: "Test",
owner_id: "3c90c3cc-1d76-27198-8888-8dd25736052a",
});
* ```
*/
export async function cloneTopic(
/** @hidden */
this: TrieveSDK,
data: CloneTopicReqPayload,
signal?: AbortSignal
) {
return await this.trieve.fetch(
"/api/topic/clone",
"post",
{
data,
datasetId: this.datasetId,
},
signal
);
}

/**
* Update an existing chat topic. Currently, only the name of the topic can be updated. Auth’ed user or api key must have an admin or owner role for the specified dataset’s organization.
*
Expand Down
43 changes: 43 additions & 0 deletions clients/ts-sdk/src/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,21 @@ export type ChunkWithPosition = {
position: number;
};

export type CloneTopicReqPayload = {
/**
* The name of the topic. If this is not provided, the topic name is the same as the previous topic
*/
name?: (string) | null;
/**
* The owner_id of the topic. This is typically a browser fingerprint or your user's id. It is used to group topics together for a user.
*/
owner_id: string;
/**
* The topic_id to clone from
*/
topic_id: string;
};

export type ClusterAnalytics = {
filter?: ((ClusterAnalyticsFilter) | null);
type: 'cluster_topics';
Expand Down Expand Up @@ -3761,6 +3776,19 @@ export type UpdateTopicData = {

export type UpdateTopicResponse = (void);

export type CloneTopicData = {
/**
* JSON request payload to create chat topic
*/
requestBody: CloneTopicReqPayload;
/**
* The dataset id or tracking_id to use for the request. We assume you intend to use an id if the value is a valid uuid.
*/
trDataset: string;
};

export type CloneTopicResponse = (Topic);

export type GetAllTopicsForOwnerIdData = {
/**
* The owner_id to get topics of; A common approach is to use a browser fingerprint or your user's id
Expand Down Expand Up @@ -5149,6 +5177,21 @@ export type $OpenApiTs = {
};
};
};
'/api/topic/clone': {
post: {
req: CloneTopicData;
res: {
/**
* The JSON response payload containing the created topic
*/
200: Topic;
/**
* Topic name empty or a service error
*/
400: ErrorResponseBody;
};
};
};
'/api/topic/owner/{owner_id}': {
get: {
req: GetAllTopicsForOwnerIdData;
Expand Down
41 changes: 41 additions & 0 deletions frontends/chat/src/components/Navbar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
BiRegularPlus,
BiRegularTrash,
BiRegularX,
BiRegularDuplicate,
} from "solid-icons/bi";
import {
Accessor,
Expand Down Expand Up @@ -81,6 +82,34 @@ export const Sidebar = (props: SidebarProps) => {
await props.refetchTopics();
};

const cloneTopic = async () => {
const dataset = userContext.currentDataset?.();
if (!dataset) return;

const res = await fetch(`${apiHost}/topic/clone`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"TR-Dataset": dataset.dataset.id,
},
body: JSON.stringify({
topic_id: props.currentTopic()?.id,
owner_id: userContext.user?.()?.id,
}),
credentials: "include",
});

if (res.ok) {
await props.refetchTopics();
} else {
createToast({
type: "error",
message: "Error deleting topic",
});
return;
}
};

const deleteSelected = async () => {
const dataset = userContext.currentDataset?.();
if (!dataset) return;
Expand Down Expand Up @@ -218,13 +247,24 @@ export const Sidebar = (props: SidebarProps) => {
<div class="flex-1" />
{props.currentTopic()?.id === topic.id && (
<div class="flex flex-row items-center space-x-2">
<button
onClick={(e) => {
e.preventDefault();
void cloneTopic();
}}
class="text-lg hover:text-blue-500"
title="Clone chat"
>
<BiRegularDuplicate />
</button>
<button
onClick={(e) => {
e.preventDefault();
setEditingTopic(topic.name);
setEditingIndex(index());
}}
class="text-lg hover:text-blue-500"
title="Edit topic name"
>
<BiRegularEdit />
</button>
Expand All @@ -234,6 +274,7 @@ export const Sidebar = (props: SidebarProps) => {
void deleteSelected();
}}
class="text-lg hover:text-red-500"
title="Delete chat"
>
<BiRegularTrash />
</button>
Expand Down
66 changes: 64 additions & 2 deletions server/src/handlers/topic_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ use crate::{
errors::ServiceError,
handlers::auth_handler::AdminOnly,
operators::{
message_operator::get_topic_string,
message_operator::{create_messages_query, get_topic_messages, get_topic_string},
topic_operator::{
create_topic_query, delete_topic_query, get_all_topics_for_owner_id_query,
update_topic_query,
get_topic_query, update_topic_query,
},
},
};
Expand Down Expand Up @@ -89,6 +89,68 @@ pub async fn create_topic(
Ok(HttpResponse::Ok().json(new_topic1))
}

#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct CloneTopicReqPayload {
/// The topic_id to clone from
pub topic_id: uuid::Uuid,
/// The name of the topic. If this is not provided, the topic name is the same as the previous topic
pub name: Option<String>,
/// The owner_id of the topic. This is typically a browser fingerprint or your user's id. It is used to group topics together for a user.
pub owner_id: String,
}

/// Clone Topic
///
/// Create a new chat topic from a `topic_id`. The new topic will be attched to the owner_id and act as a coordinator for conversation message history of gen-AI chat sessions. Auth'ed user or api key must have an admin or owner role for the specified dataset's organization.
#[utoipa::path(
post,
path = "/topic/clone",
context_path = "/api",
tag = "Topic",
request_body(content = CloneTopicReqPayload, description = "JSON request payload to create chat topic", content_type = "application/json"),
responses(
(status = 200, description = "The JSON response payload containing the created topic", body = Topic),
(status = 400, description = "Topic name empty or a service error", body = ErrorResponseBody),
),
params(
("TR-Dataset" = String, Header, description = "The dataset id or tracking_id to use for the request. We assume you intend to use an id if the value is a valid uuid."),
),
security(
("ApiKey" = ["admin"]),
)
)]
#[tracing::instrument(skip(pool))]
pub async fn clone_topic(
data: web::Json<CloneTopicReqPayload>,
user: AdminOnly,
dataset_org_plan_sub: DatasetAndOrgWithSubAndPlan,
pool: web::Data<Pool>,
) -> Result<HttpResponse, actix_web::Error> {
let data = data.into_inner();

// get topic from topic_id
let original_topic =
get_topic_query(data.topic_id, dataset_org_plan_sub.dataset.id, &pool).await?;

let topic_name = data.name.unwrap_or(original_topic.name);

let new_topic = Topic::from_details(topic_name, data.owner_id, dataset_org_plan_sub.dataset.id);

create_topic_query(new_topic.clone(), &pool).await?;

let mut old_messages =
get_topic_messages(original_topic.id, dataset_org_plan_sub.dataset.id, &pool).await?;

old_messages.iter_mut().for_each(|message| {
message.topic_id = new_topic.id;
message.id = uuid::Uuid::new_v4();
});

create_messages_query(old_messages, &pool).await?;

Ok(HttpResponse::Ok().json(new_topic))
}

#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct DeleteTopicData {
/// The id of the topic to target.
Expand Down
6 changes: 6 additions & 0 deletions server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ impl Modify for SecurityAddon {
handlers::topic_handler::create_topic,
handlers::topic_handler::delete_topic,
handlers::topic_handler::update_topic,
handlers::topic_handler::clone_topic,
handlers::topic_handler::get_all_topics_for_owner_id,
handlers::message_handler::create_message,
handlers::message_handler::get_all_topic_messages,
Expand Down Expand Up @@ -250,6 +251,7 @@ impl Modify for SecurityAddon {
schemas(
handlers::auth_handler::AuthQuery,
handlers::topic_handler::CreateTopicReqPayload,
handlers::topic_handler::CloneTopicReqPayload,
handlers::topic_handler::DeleteTopicData,
handlers::topic_handler::UpdateTopicReqPayload,
handlers::message_handler::CreateMessageReqPayload,
Expand Down Expand Up @@ -836,6 +838,10 @@ pub fn main() -> std::io::Result<()> {
.route(web::post().to(handlers::topic_handler::create_topic))
.route(web::put().to(handlers::topic_handler::update_topic)),
)
.service(
web::resource("/topic/clone")
.route(web::post().to(handlers::topic_handler::clone_topic))
)
.service(
web::resource("/topic/{topic_id}")
.route(web::delete().to(handlers::topic_handler::delete_topic)),
Expand Down
Loading
Loading