Skip to content

Commit

Permalink
[Ahuna] MemberAgentVisibility table (#2674)
Browse files Browse the repository at this point in the history
* table model

* constraints

* updated constraints

* post method

* delete

* rename

* move

* initdb

* assistantId

* spolu review

* renaming after discussion
  • Loading branch information
philipperolet authored Nov 27, 2023
1 parent 14f20bf commit 479700e
Show file tree
Hide file tree
Showing 6 changed files with 362 additions and 2 deletions.
6 changes: 5 additions & 1 deletion front/admin/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ import {
XP1Run,
XP1User,
} from "@app/lib/models";
import { GlobalAgentSettings } from "@app/lib/models/assistant/agent";
import {
GlobalAgentSettings,
MemberAgentVisibility,
} from "@app/lib/models/assistant/agent";
import { ContentFragment } from "@app/lib/models/assistant/conversation";
import { PlanInvitation } from "@app/lib/models/plan";

Expand Down Expand Up @@ -69,6 +72,7 @@ async function main() {
await AgentRetrievalConfiguration.sync({ alter: true });
await AgentDataSourceConfiguration.sync({ alter: true });
await AgentConfiguration.sync({ alter: true });
await MemberAgentVisibility.sync({ alter: true });
await GlobalAgentSettings.sync({ alter: true });

await AgentRetrievalAction.sync({ alter: true });
Expand Down
76 changes: 76 additions & 0 deletions front/lib/api/assistant/visibility_override.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { getAgentConfiguration } from "@app/lib/api/assistant/configuration";
import { Authenticator } from "@app/lib/auth";
import { Membership } from "@app/lib/models";
import { MemberAgentVisibility } from "@app/lib/models/assistant/agent";
import { Err, Ok, Result } from "@app/lib/result";
import { AgentVisibilityOverrideType } from "@app/types/assistant/agent";

export async function setVisibilityOverrideForUser({
auth,
agentId,
visibility,
}: {
auth: Authenticator;
agentId: string;
visibility: AgentVisibilityOverrideType;
}): Promise<
Result<
{ visibility: AgentVisibilityOverrideType; created: boolean | null },
Error
>
> {
const agentConfiguration = await getAgentConfiguration(auth, agentId);
if (!agentConfiguration)
return new Err(new Error(`Could not find agent configuration ${agentId}`));
const [userId, workspaceId] = [auth.user()?.id, auth.workspace()?.id];
const membership = await Membership.findOne({
where: { userId, workspaceId },
});
if (!membership)
return new Err(
new Error(`Could not find membership (user ${userId} ws ${workspaceId})`)
);

const [memberAgentVisibility, created] = await MemberAgentVisibility.upsert({
membershipId: membership.id,
agentConfigurationId: agentConfiguration.id,
visibility: visibility,
});
return new Ok({ visibility: memberAgentVisibility.visibility, created });
}

export async function deleteVisibilityOverrideForUser({
auth,
agentId,
}: {
auth: Authenticator;
agentId: string;
}): Promise<Result<{ success: boolean }, Error>> {
const agentConfiguration = await getAgentConfiguration(auth, agentId);
if (!agentConfiguration)
return new Err(new Error(`Could not find agent configuration ${agentId}`));
const [userId, workspaceId] = [auth.user()?.id, auth.workspace()?.id];
const membership = await Membership.findOne({
where: { userId, workspaceId },
});
if (!membership)
return new Err(
new Error(`Could not find membership (user ${userId} ws ${workspaceId})`)
);

if (agentConfiguration.scope === "private") {
return new Err(
new Error(
"Cannot remove visibility entry for a 'private'-scope assistant. Please delete the assistant instead."
)
);
}
await MemberAgentVisibility.destroy({
where: {
membershipId: membership.id,
agentConfigurationId: agentConfiguration.id,
},
});

return new Ok({ success: true });
}
1 change: 1 addition & 0 deletions front/lib/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type APIErrorType =
| "workspace_not_found"
| "action_unknown_error"
| "action_api_error"
| "membership_not_found"
| "invitation_not_found"
| "plan_limit_error"
| "template_not_found"
Expand Down
82 changes: 81 additions & 1 deletion front/lib/models/assistant/agent.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from "assert";
import {
CreationOptional,
DataTypes,
Expand All @@ -10,11 +11,12 @@ import {

import { front_sequelize } from "@app/lib/databases";
import { AgentRetrievalConfiguration } from "@app/lib/models/assistant/actions/retrieval";
import { Workspace } from "@app/lib/models/workspace";
import { Membership, Workspace } from "@app/lib/models/workspace";
import { DustAppRunConfigurationType } from "@app/types/assistant/actions/dust_app_run";
import {
AgentConfigurationScope,
AgentStatus,
AgentVisibilityOverrideType,
GlobalAgentStatus,
} from "@app/types/assistant/agent";

Expand Down Expand Up @@ -284,3 +286,81 @@ Workspace.hasMany(GlobalAgentSettings, {
GlobalAgentSettings.belongsTo(Workspace, {
foreignKey: { name: "workspaceId", allowNull: false },
});

export class MemberAgentVisibility extends Model<
InferAttributes<MemberAgentVisibility>,
InferCreationAttributes<MemberAgentVisibility>
> {
declare id: CreationOptional<number>;
declare createdAt: CreationOptional<Date>;
declare updatedAt: CreationOptional<Date>;

declare visibility: AgentVisibilityOverrideType;

declare membershipId: ForeignKey<Membership["id"]>;
declare agentConfigurationId: ForeignKey<AgentConfiguration["id"]>;
}

MemberAgentVisibility.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
},

visibility: {
type: DataTypes.STRING,
allowNull: false,
},
},
{
modelName: "member_agent_visibility",
sequelize: front_sequelize,
indexes: [
{ fields: ["membershipId"] },
{ fields: ["agentConfigurationId", "membershipId"], unique: true },
],
}
);

Membership.hasMany(MemberAgentVisibility, {
foreignKey: { allowNull: false },
onDelete: "CASCADE",
});
AgentConfiguration.hasMany(MemberAgentVisibility, {
foreignKey: { allowNull: false },
onDelete: "CASCADE",
});

MemberAgentVisibility.addHook(
"beforeCreate",
"only_one_listing_for_private_agent",
async (memberAgentList) => {
const agentConfiguration = await AgentConfiguration.findOne({
where: {
id: memberAgentList.dataValues.agentConfigurationId,
},
});
if (!agentConfiguration)
throw new Error("Unexpected: Agent configuration not found");
if (agentConfiguration.scope === "private") {
const existingMemberAgentList = await MemberAgentVisibility.findOne({
where: {
agentConfigurationId: memberAgentList.dataValues.agentConfigurationId,
},
});
assert(!existingMemberAgentList);
}
}
);
192 changes: 192 additions & 0 deletions front/pages/api/w/[wId]/members/me/assistant_visibility_override.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { isLeft } from "fp-ts/lib/Either";
import * as t from "io-ts";
import * as reporter from "io-ts-reporters";
import { NextApiRequest, NextApiResponse } from "next";

import { getAgentConfiguration } from "@app/lib/api/assistant/configuration";
import {
deleteVisibilityOverrideForUser,
setVisibilityOverrideForUser,
} from "@app/lib/api/assistant/visibility_override";
import { Authenticator, getSession } from "@app/lib/auth";
import { Membership } from "@app/lib/models";
import { apiError, withLogging } from "@app/logger/withlogging";
import { AgentVisibilityOverrideType } from "@app/types/assistant/agent";

export type PostMemberAssistantVisibilityResponseBody = {
created: boolean | null;
visibility: AgentVisibilityOverrideType;
};

export const PostMemberAssistantVisibilityRequestBodySchema = t.type({
assistantId: t.string,
visibility: t.union([
t.literal("workspace-unlisted"),
t.literal("published-listed"),
]),
});

export const DeleteMemberAssistantsVisibilityBodySchema = t.type({
assistantId: t.string,
});

async function handler(
req: NextApiRequest,
res: NextApiResponse<
PostMemberAssistantVisibilityResponseBody | { success: boolean }
>
): Promise<void> {
const session = await getSession(req, res);
const auth = await Authenticator.fromSession(
session,
req.query.wId as string
);

const owner = auth.workspace();
if (!owner) {
return apiError(req, res, {
status_code: 404,
api_error: {
type: "workspace_not_found",
message: "The workspace was not found.",
},
});
}

const user = auth.user();
if (!user) {
return apiError(req, res, {
status_code: 404,
api_error: {
type: "workspace_user_not_found",
message: "The user requested was not found.",
},
});
}

switch (req.method) {
case "POST":
const bodyValidation =
PostMemberAssistantVisibilityRequestBodySchema.decode(req.body);
if (isLeft(bodyValidation)) {
const pathError = reporter.formatValidationErrors(bodyValidation.left);

return apiError(req, res, {
status_code: 400,
api_error: {
type: "invalid_request_error",
message: `Invalid request body: ${pathError}`,
},
});
}

const { assistantId, visibility } = bodyValidation.right;
const agentConfiguration = await getAgentConfiguration(auth, assistantId);
if (!agentConfiguration) {
return apiError(req, res, {
status_code: 404,
api_error: {
type: "agent_configuration_not_found",
message: "The agent requested was not found.",
},
});
}
const [userId, workspaceId] = [auth.user()?.id, auth.workspace()?.id];
const membership = await Membership.findOne({
where: { userId, workspaceId },
});
if (!membership) {
return apiError(req, res, {
status_code: 404,
api_error: {
type: "membership_not_found",
message: "The membership requested was not found.",
},
});
}
const result = await setVisibilityOverrideForUser({
auth,
agentId: assistantId,
visibility,
});
if (result.isErr()) {
return apiError(req, res, {
status_code: 500,
api_error: {
type: "internal_server_error",
message: result.error.message,
},
});
}
res.status(200).json(result.value);
return;
case "DELETE":
const bodyValidationDelete =
DeleteMemberAssistantsVisibilityBodySchema.decode(req.body);
if (isLeft(bodyValidationDelete)) {
const pathError = reporter.formatValidationErrors(
bodyValidationDelete.left
);

return apiError(req, res, {
status_code: 400,
api_error: {
type: "invalid_request_error",
message: `Invalid request body: ${pathError}`,
},
});
}
const { assistantId: assistantIdToDelete } = bodyValidationDelete.right;
const deleteAgentConfiguration = await getAgentConfiguration(
auth,
assistantIdToDelete
);
if (!deleteAgentConfiguration) {
return apiError(req, res, {
status_code: 404,
api_error: {
type: "agent_configuration_not_found",
message: "The agent requested was not found.",
},
});
}
const delMembership = await Membership.findOne({
where: { userId: auth.user()?.id, workspaceId: auth.workspace()?.id },
});
if (!delMembership) {
return apiError(req, res, {
status_code: 404,
api_error: {
type: "membership_not_found",
message: "The membership requested was not found.",
},
});
}

const deleteResult = await deleteVisibilityOverrideForUser({
auth,
agentId: assistantIdToDelete,
});
if (deleteResult.isErr()) {
return apiError(req, res, {
status_code: 500,
api_error: {
type: "internal_server_error",
message: deleteResult.error.message,
},
});
}
res.status(200).json(deleteResult.value);
return;
default:
return apiError(req, res, {
status_code: 405,
api_error: {
type: "method_not_supported_error",
message: "The method passed is not supported, POST is expected.",
},
});
}
}

export default withLogging(handler);
Loading

0 comments on commit 479700e

Please sign in to comment.