parent
e32c9a814a
commit
06f27e8d6a
@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ai_sessions_metadata" ADD COLUMN "updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX IF EXISTS "ai_session_unique_doc_session_idx";
|
@ -443,6 +443,7 @@ model AiSession {
|
|||||||
messageCost Int @default(0)
|
messageCost Int @default(0)
|
||||||
tokenCost Int @default(0)
|
tokenCost Int @default(0)
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(3)
|
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(3)
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
@ -67,6 +67,49 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
## should validate session prompt compatibility
|
||||||
|
|
||||||
|
> session prompt validation results
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
promptType: 'non-action',
|
||||||
|
result: 'success',
|
||||||
|
sessionType: 'workspace',
|
||||||
|
shouldThrow: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
promptType: 'action',
|
||||||
|
result: 'CopilotPromptInvalid',
|
||||||
|
sessionType: 'workspace',
|
||||||
|
shouldThrow: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
promptType: 'non-action',
|
||||||
|
result: 'success',
|
||||||
|
sessionType: 'pinned',
|
||||||
|
shouldThrow: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
promptType: 'action',
|
||||||
|
result: 'CopilotPromptInvalid',
|
||||||
|
sessionType: 'pinned',
|
||||||
|
shouldThrow: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
promptType: 'non-action',
|
||||||
|
result: 'success',
|
||||||
|
sessionType: 'doc',
|
||||||
|
shouldThrow: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
promptType: 'action',
|
||||||
|
result: 'success',
|
||||||
|
sessionType: 'doc',
|
||||||
|
shouldThrow: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
## should pin and unpin sessions
|
## should pin and unpin sessions
|
||||||
|
|
||||||
> session states after creating second pinned session
|
> session states after creating second pinned session
|
||||||
@ -105,6 +148,345 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
## should handle session updates and type conversions
|
||||||
|
|
||||||
|
> session update validation results
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
result: 'rejected',
|
||||||
|
sessionType: 'action',
|
||||||
|
update: {
|
||||||
|
docId: 'new-doc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
result: 'rejected',
|
||||||
|
sessionType: 'action',
|
||||||
|
update: {
|
||||||
|
pinned: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
result: 'rejected',
|
||||||
|
sessionType: 'action',
|
||||||
|
update: {
|
||||||
|
promptName: 'test-prompt',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
result: 'success',
|
||||||
|
sessionType: 'forked',
|
||||||
|
update: {
|
||||||
|
pinned: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
result: 'success',
|
||||||
|
sessionType: 'forked',
|
||||||
|
update: {
|
||||||
|
promptName: 'test-prompt',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
result: 'rejected',
|
||||||
|
sessionType: 'forked',
|
||||||
|
update: {
|
||||||
|
docId: 'new-doc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
result: 'success',
|
||||||
|
sessionType: 'regular',
|
||||||
|
update: {
|
||||||
|
promptName: 'test-prompt',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
result: 'rejected',
|
||||||
|
sessionType: 'regular',
|
||||||
|
update: {
|
||||||
|
promptName: 'action-prompt',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
result: 'rejected',
|
||||||
|
sessionType: 'regular',
|
||||||
|
update: {
|
||||||
|
promptName: 'non-existent-prompt',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
> pinning behavior - should unpin existing when pinning new
|
||||||
|
|
||||||
|
{
|
||||||
|
onlyOneSessionPinned: true,
|
||||||
|
pinnedSessions: 1,
|
||||||
|
totalSessions: 2,
|
||||||
|
unpinnedSessions: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
> session type conversion steps
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
sessionState: {
|
||||||
|
hasDocId: true,
|
||||||
|
pinned: false,
|
||||||
|
},
|
||||||
|
step: 'workspace_to_doc',
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sessionState: {
|
||||||
|
hasDocId: false,
|
||||||
|
pinned: false,
|
||||||
|
},
|
||||||
|
step: 'doc_to_workspace',
|
||||||
|
type: 'workspace',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sessionState: {
|
||||||
|
hasDocId: false,
|
||||||
|
pinned: true,
|
||||||
|
},
|
||||||
|
step: 'workspace_to_pinned',
|
||||||
|
type: 'pinned',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
## should handle session queries, ordering, and filtering
|
||||||
|
|
||||||
|
> comprehensive session query results
|
||||||
|
|
||||||
|
{
|
||||||
|
all_workspace_sessions: {
|
||||||
|
count: 2,
|
||||||
|
sessionTypes: [
|
||||||
|
{
|
||||||
|
hasMessages: false,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 0,
|
||||||
|
type: 'pinned',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hasMessages: false,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 0,
|
||||||
|
type: 'workspace',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
doc_sessions_with_messages: {
|
||||||
|
count: 5,
|
||||||
|
sessionTypes: [
|
||||||
|
{
|
||||||
|
hasMessages: false,
|
||||||
|
isFork: true,
|
||||||
|
messageCount: 0,
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hasMessages: true,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 1,
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hasMessages: true,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 1,
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hasMessages: false,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 0,
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hasMessages: true,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 1,
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
latest_valid_session: {
|
||||||
|
count: 1,
|
||||||
|
sessionTypes: [
|
||||||
|
{
|
||||||
|
hasMessages: false,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 0,
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
non_action_sessions: {
|
||||||
|
count: 4,
|
||||||
|
sessionTypes: [
|
||||||
|
{
|
||||||
|
hasMessages: false,
|
||||||
|
isFork: true,
|
||||||
|
messageCount: 0,
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hasMessages: false,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 0,
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hasMessages: false,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 0,
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hasMessages: false,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 0,
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
non_fork_sessions: {
|
||||||
|
count: 4,
|
||||||
|
sessionTypes: [
|
||||||
|
{
|
||||||
|
hasMessages: false,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 0,
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hasMessages: false,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 0,
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hasMessages: false,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 0,
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hasMessages: false,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 0,
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
recent_top3_sessions: {
|
||||||
|
count: 2,
|
||||||
|
sessionTypes: [
|
||||||
|
{
|
||||||
|
hasMessages: false,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 0,
|
||||||
|
type: 'pinned',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hasMessages: false,
|
||||||
|
isFork: false,
|
||||||
|
messageCount: 0,
|
||||||
|
type: 'workspace',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
> session type identification results
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
session: {
|
||||||
|
docId: null,
|
||||||
|
pinned: false,
|
||||||
|
},
|
||||||
|
type: 'workspace',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
session: {
|
||||||
|
docId: undefined,
|
||||||
|
pinned: false,
|
||||||
|
},
|
||||||
|
type: 'workspace',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
session: {
|
||||||
|
docId: null,
|
||||||
|
pinned: true,
|
||||||
|
},
|
||||||
|
type: 'pinned',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
session: {
|
||||||
|
docId: 'test-doc-id',
|
||||||
|
pinned: false,
|
||||||
|
},
|
||||||
|
type: 'doc',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
## should handle fork and session attachment operations
|
||||||
|
|
||||||
|
> fork operation results
|
||||||
|
|
||||||
|
{
|
||||||
|
existingPinnedSessionUnpinned: true,
|
||||||
|
forkResults: [
|
||||||
|
{
|
||||||
|
actualState: {
|
||||||
|
hasDocId: false,
|
||||||
|
hasParent: true,
|
||||||
|
isDocIdCorrect: true,
|
||||||
|
pinned: false,
|
||||||
|
},
|
||||||
|
description: 'workspace fork',
|
||||||
|
success: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actualState: {
|
||||||
|
hasDocId: true,
|
||||||
|
hasParent: true,
|
||||||
|
isDocIdCorrect: true,
|
||||||
|
pinned: false,
|
||||||
|
},
|
||||||
|
description: 'doc fork',
|
||||||
|
success: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actualState: {
|
||||||
|
hasDocId: false,
|
||||||
|
hasParent: true,
|
||||||
|
isDocIdCorrect: true,
|
||||||
|
pinned: true,
|
||||||
|
},
|
||||||
|
description: 'pinned fork',
|
||||||
|
success: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
> attach and detach operation results
|
||||||
|
|
||||||
|
{
|
||||||
|
attachPhase: {
|
||||||
|
bothSessionsPresent: true,
|
||||||
|
docSessionCount: 2,
|
||||||
|
},
|
||||||
|
detachPhase: {
|
||||||
|
originalDocSessionRemains: true,
|
||||||
|
workspaceSessionExists: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
## should handle session updates and validations
|
## should handle session updates and validations
|
||||||
|
|
||||||
> should unpin existing when pinning new session
|
> should unpin existing when pinning new session
|
||||||
@ -130,7 +512,7 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
docId: 'doc-update-id',
|
docId: 'doc-update-id',
|
||||||
pinned: false,
|
pinned: false,
|
||||||
},
|
},
|
||||||
step: 'pinned_to_doc',
|
step: 'workspace_to_doc',
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -151,64 +533,55 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
## session updates and type conversions
|
## should create multiple doc sessions and query latest
|
||||||
|
|
||||||
> session states after pinning - should unpin existing
|
> multiple doc sessions for same document with order verification
|
||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
docId: null,
|
docId: 'multi-session-doc',
|
||||||
id: 'session-update-id',
|
hasMessages: true,
|
||||||
pinned: true,
|
isFirstSession: false,
|
||||||
|
isSecondSession: false,
|
||||||
|
isThirdSession: true,
|
||||||
|
messageCount: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
docId: null,
|
docId: 'multi-session-doc',
|
||||||
id: 'existing-pinned-session-id',
|
hasMessages: true,
|
||||||
pinned: false,
|
isFirstSession: false,
|
||||||
|
isSecondSession: true,
|
||||||
|
isThirdSession: false,
|
||||||
|
messageCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
docId: 'multi-session-doc',
|
||||||
|
hasMessages: true,
|
||||||
|
isFirstSession: true,
|
||||||
|
isSecondSession: false,
|
||||||
|
isThirdSession: false,
|
||||||
|
messageCount: 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
> session state after unpinning
|
## should query recent topK sessions of different types
|
||||||
|
|
||||||
{
|
> should include different session types in recent topK query
|
||||||
docId: null,
|
|
||||||
id: 'session-update-id',
|
|
||||||
pinned: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
> session type conversion steps
|
|
||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
session: {
|
|
||||||
docId: 'doc-update-id',
|
|
||||||
pinned: false,
|
|
||||||
},
|
|
||||||
step: 'workspace_to_doc',
|
|
||||||
type: 'doc',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
session: {
|
|
||||||
docId: 'doc-update-id',
|
|
||||||
pinned: true,
|
|
||||||
},
|
|
||||||
step: 'doc_to_pinned',
|
|
||||||
type: 'pinned',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
session: {
|
|
||||||
docId: null,
|
docId: null,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
},
|
|
||||||
step: 'pinned_to_workspace',
|
|
||||||
type: 'workspace',
|
type: 'workspace',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
session: {
|
|
||||||
docId: null,
|
docId: null,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
},
|
|
||||||
step: 'workspace_to_pinned',
|
|
||||||
type: 'pinned',
|
type: 'pinned',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
docId: null,
|
||||||
|
pinned: false,
|
||||||
|
type: 'workspace',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
Binary file not shown.
@ -47,13 +47,20 @@ test.after(async t => {
|
|||||||
await t.context.module.close();
|
await t.context.module.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Test data constants
|
||||||
|
const TEST_PROMPTS = {
|
||||||
|
NORMAL: 'test-prompt',
|
||||||
|
ACTION: 'action-prompt',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
const createTestPrompts = async (
|
const createTestPrompts = async (
|
||||||
copilotSession: CopilotSessionModel,
|
copilotSession: CopilotSessionModel,
|
||||||
db: PrismaClient
|
db: PrismaClient
|
||||||
) => {
|
) => {
|
||||||
await copilotSession.createPrompt('test-prompt', 'gpt-4.1');
|
await copilotSession.createPrompt(TEST_PROMPTS.NORMAL, 'gpt-4.1');
|
||||||
await db.aiPrompt.create({
|
await db.aiPrompt.create({
|
||||||
data: { name: 'action-prompt', model: 'gpt-4.1', action: 'edit' },
|
data: { name: TEST_PROMPTS.ACTION, model: 'gpt-4.1', action: 'edit' },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -75,7 +82,7 @@ const createTestSession = async (
|
|||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId: null,
|
docId: null,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
promptName: 'test-prompt',
|
promptName: TEST_PROMPTS.NORMAL,
|
||||||
promptAction: null,
|
promptAction: null,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
@ -84,14 +91,62 @@ const createTestSession = async (
|
|||||||
return sessionData;
|
return sessionData;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSessionState = async (db: PrismaClient, sessionId: string) => {
|
const getSessionStates = async (db: PrismaClient, sessionIds: string[]) => {
|
||||||
const session = await db.aiSession.findUnique({
|
const sessions = await Promise.all(
|
||||||
where: { id: sessionId },
|
sessionIds.map(id =>
|
||||||
|
db.aiSession.findUnique({
|
||||||
|
where: { id },
|
||||||
select: { id: true, pinned: true, docId: true },
|
select: { id: true, pinned: true, docId: true },
|
||||||
});
|
})
|
||||||
return session;
|
)
|
||||||
|
);
|
||||||
|
return sessions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addMessagesToSession = async (
|
||||||
|
copilotSession: CopilotSessionModel,
|
||||||
|
sessionId: string,
|
||||||
|
content: string,
|
||||||
|
delayMs: number = 0
|
||||||
|
) => {
|
||||||
|
if (delayMs > 0) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||||
|
}
|
||||||
|
await copilotSession.updateMessages({
|
||||||
|
sessionId,
|
||||||
|
userId: user.id,
|
||||||
|
prompt: { model: 'gpt-4.1' },
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content,
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSessionWithMessages = async (
|
||||||
|
t: ExecutionContext<Context>,
|
||||||
|
overrides: Parameters<typeof createTestSession>[1] = {},
|
||||||
|
messageContent?: string,
|
||||||
|
delayMs: number = 0
|
||||||
|
) => {
|
||||||
|
const sessionData = await createTestSession(t, overrides);
|
||||||
|
if (messageContent) {
|
||||||
|
await addMessagesToSession(
|
||||||
|
t.context.copilotSession,
|
||||||
|
sessionData.sessionId,
|
||||||
|
messageContent,
|
||||||
|
delayMs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return sessionData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simplified update assertion helpers
|
||||||
|
type UpdateData = Omit<UpdateChatSessionOptions, 'userId' | 'sessionId'>;
|
||||||
|
|
||||||
test('should list and filter session type', async t => {
|
test('should list and filter session type', async t => {
|
||||||
const { copilotSession, db } = t.context;
|
const { copilotSession, db } = t.context;
|
||||||
|
|
||||||
@ -128,12 +183,23 @@ test('should list and filter session type', async t => {
|
|||||||
docId,
|
docId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
t.is(
|
||||||
|
docSessions.length,
|
||||||
|
2,
|
||||||
|
'should return exactly 2 doc sessions for the specified docId'
|
||||||
|
);
|
||||||
|
|
||||||
|
t.true(
|
||||||
|
docSessions.every(s => s.docId === docId),
|
||||||
|
'all returned sessions should have the specified docId'
|
||||||
|
);
|
||||||
|
|
||||||
t.snapshot(
|
t.snapshot(
|
||||||
cleanObject(
|
cleanObject(
|
||||||
docSessions.toSorted(s =>
|
docSessions.toSorted((a, b) =>
|
||||||
s.docId!.localeCompare(s.docId!, undefined, { numeric: true })
|
a.promptName.localeCompare(b.promptName)
|
||||||
),
|
),
|
||||||
['id', 'userId', 'workspaceId', 'createdAt', 'tokenCost']
|
['id', 'userId', 'workspaceId', 'createdAt', 'updatedAt', 'tokenCost']
|
||||||
),
|
),
|
||||||
'doc sessions should only include sessions with matching docId'
|
'doc sessions should only include sessions with matching docId'
|
||||||
);
|
);
|
||||||
@ -158,65 +224,58 @@ test('should list and filter session type', async t => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should check session validation for prompts', async t => {
|
test('should validate session prompt compatibility', async t => {
|
||||||
const { copilotSession, db } = t.context;
|
const { copilotSession, db } = t.context;
|
||||||
|
|
||||||
await createTestPrompts(copilotSession, db);
|
await createTestPrompts(copilotSession, db);
|
||||||
|
|
||||||
const docId = randomUUID();
|
|
||||||
const sessionTypes = [
|
const sessionTypes = [
|
||||||
{ name: 'workspace', session: { docId: null, pinned: false } },
|
{ name: 'workspace', session: { docId: null, pinned: false } },
|
||||||
{ name: 'pinned', session: { docId: null, pinned: true } },
|
{ name: 'pinned', session: { docId: null, pinned: true } },
|
||||||
{ name: 'doc', session: { docId, pinned: false } },
|
{ name: 'doc', session: { docId: randomUUID(), pinned: false } },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const result = sessionTypes.flatMap(({ name, session }) => [
|
||||||
// non-action prompts should work for all session types
|
// non-action prompts should work for all session types
|
||||||
sessionTypes.forEach(({ name, session }) => {
|
{
|
||||||
t.notThrows(
|
sessionType: name,
|
||||||
() =>
|
promptType: 'non-action',
|
||||||
|
shouldThrow: false,
|
||||||
|
result: (() => {
|
||||||
|
try {
|
||||||
copilotSession.checkSessionPrompt(session, {
|
copilotSession.checkSessionPrompt(session, {
|
||||||
name: 'test-prompt',
|
name: TEST_PROMPTS.NORMAL,
|
||||||
action: undefined,
|
action: undefined,
|
||||||
}),
|
|
||||||
`${name} session should allow non-action prompts`
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
return 'success';
|
||||||
|
} catch (error) {
|
||||||
|
return error instanceof CopilotPromptInvalid
|
||||||
|
? 'CopilotPromptInvalid'
|
||||||
|
: 'unknown';
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
},
|
||||||
// action prompts should only work for doc session type
|
// action prompts should only work for doc session type
|
||||||
{
|
{
|
||||||
const actionPromptTests = [
|
sessionType: name,
|
||||||
{
|
promptType: 'action',
|
||||||
name: 'workspace',
|
shouldThrow: name !== 'doc',
|
||||||
session: sessionTypes[0].session,
|
result: (() => {
|
||||||
shouldThrow: true,
|
try {
|
||||||
},
|
|
||||||
{ name: 'pinned', session: sessionTypes[1].session, shouldThrow: true },
|
|
||||||
{ name: 'doc', session: sessionTypes[2].session, shouldThrow: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
actionPromptTests.forEach(({ name, session, shouldThrow }) => {
|
|
||||||
if (shouldThrow) {
|
|
||||||
t.throws(
|
|
||||||
() =>
|
|
||||||
copilotSession.checkSessionPrompt(session, {
|
copilotSession.checkSessionPrompt(session, {
|
||||||
name: 'action-prompt',
|
name: TEST_PROMPTS.ACTION,
|
||||||
action: 'edit',
|
action: 'edit',
|
||||||
}),
|
|
||||||
{ instanceOf: CopilotPromptInvalid },
|
|
||||||
`${name} session should reject action prompts`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
t.notThrows(
|
|
||||||
() =>
|
|
||||||
copilotSession.checkSessionPrompt(session, {
|
|
||||||
name: 'action-prompt',
|
|
||||||
action: 'edit',
|
|
||||||
}),
|
|
||||||
`${name} session should allow action prompts`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
return 'success';
|
||||||
|
} catch (error) {
|
||||||
|
return error instanceof CopilotPromptInvalid
|
||||||
|
? 'CopilotPromptInvalid'
|
||||||
|
: 'unknown';
|
||||||
}
|
}
|
||||||
|
})(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
t.snapshot(result, 'session prompt validation results');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should pin and unpin sessions', async t => {
|
test('should pin and unpin sessions', async t => {
|
||||||
@ -255,9 +314,9 @@ test('should pin and unpin sessions', async t => {
|
|||||||
pinned: true,
|
pinned: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionStatesAfterSecondPin = await Promise.all([
|
const sessionStatesAfterSecondPin = await getSessionStates(db, [
|
||||||
getSessionState(db, firstSessionId),
|
firstSessionId,
|
||||||
getSessionState(db, secondSessionId),
|
secondSessionId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
t.snapshot(
|
t.snapshot(
|
||||||
@ -298,177 +357,553 @@ test('should pin and unpin sessions', async t => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle session updates and validations', async t => {
|
test('should handle session updates and type conversions', async t => {
|
||||||
const { copilotSession, db } = t.context;
|
const { copilotSession, db } = t.context;
|
||||||
await createTestPrompts(copilotSession, db);
|
await createTestPrompts(copilotSession, db);
|
||||||
|
|
||||||
const sessionId = 'session-update-id';
|
const sessionId = randomUUID();
|
||||||
const actionSessionId = 'action-session-id';
|
const actionSessionId = randomUUID();
|
||||||
const parentSessionId = 'parent-session-id';
|
const forkedSessionId = randomUUID();
|
||||||
const forkedSessionId = 'forked-session-id';
|
const parentSessionId = randomUUID();
|
||||||
const docId = 'doc-update-id';
|
const docId = randomUUID();
|
||||||
|
|
||||||
|
{
|
||||||
await createTestSession(t, { sessionId });
|
await createTestSession(t, { sessionId });
|
||||||
await createTestSession(t, {
|
await createTestSession(t, {
|
||||||
sessionId: actionSessionId,
|
sessionId: actionSessionId,
|
||||||
promptName: 'action-prompt',
|
promptName: TEST_PROMPTS.ACTION,
|
||||||
promptAction: 'edit',
|
promptAction: 'edit',
|
||||||
docId: 'some-doc',
|
docId,
|
||||||
});
|
|
||||||
await createTestSession(t, {
|
|
||||||
sessionId: parentSessionId,
|
|
||||||
docId: 'parent-doc',
|
|
||||||
});
|
});
|
||||||
|
await createTestSession(t, { sessionId: parentSessionId, docId });
|
||||||
await db.aiSession.create({
|
await db.aiSession.create({
|
||||||
data: {
|
data: {
|
||||||
id: forkedSessionId,
|
id: forkedSessionId,
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
docId: 'forked-doc',
|
docId,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
promptName: 'test-prompt',
|
promptName: TEST_PROMPTS.NORMAL,
|
||||||
promptAction: null,
|
promptAction: null,
|
||||||
parentSessionId: parentSessionId,
|
parentSessionId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
type UpdateData = Omit<UpdateChatSessionOptions, 'userId' | 'sessionId'>;
|
const updateTestCases = [
|
||||||
const assertUpdateThrows = async (
|
// action sessions should reject all updates
|
||||||
t: ExecutionContext<Context>,
|
|
||||||
sessionId: string,
|
|
||||||
updateData: UpdateData,
|
|
||||||
message: string
|
|
||||||
) => {
|
|
||||||
await t.throwsAsync(
|
|
||||||
t.context.copilotSession.update({
|
|
||||||
...updateData,
|
|
||||||
userId: user.id,
|
|
||||||
sessionId,
|
|
||||||
}),
|
|
||||||
{ instanceOf: CopilotSessionInvalidInput },
|
|
||||||
message
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const assertUpdate = async (
|
|
||||||
t: ExecutionContext<Context>,
|
|
||||||
sessionId: string,
|
|
||||||
updateData: UpdateData,
|
|
||||||
message: string
|
|
||||||
) => {
|
|
||||||
await t.notThrowsAsync(
|
|
||||||
t.context.copilotSession.update({
|
|
||||||
...updateData,
|
|
||||||
userId: user.id,
|
|
||||||
sessionId,
|
|
||||||
}),
|
|
||||||
message
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// case 1: action sessions should reject all updates
|
|
||||||
{
|
{
|
||||||
const actionUpdates = [
|
sessionId: actionSessionId,
|
||||||
{ docId: 'new-doc' },
|
updates: [
|
||||||
{ pinned: true },
|
{ docId: 'new-doc', expected: 'reject' },
|
||||||
{ promptName: 'test-prompt' },
|
{ pinned: true, expected: 'reject' },
|
||||||
|
{ promptName: TEST_PROMPTS.NORMAL, expected: 'reject' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// forked sessions should reject docId updates but allow others
|
||||||
|
{
|
||||||
|
sessionId: forkedSessionId,
|
||||||
|
updates: [
|
||||||
|
{ pinned: true, expected: 'allow' },
|
||||||
|
{ promptName: TEST_PROMPTS.NORMAL, expected: 'allow' },
|
||||||
|
{ docId: 'new-doc', expected: 'reject' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Regular sessions - prompt validation
|
||||||
|
{
|
||||||
|
sessionId,
|
||||||
|
updates: [
|
||||||
|
{ promptName: TEST_PROMPTS.NORMAL, expected: 'allow' },
|
||||||
|
{ promptName: TEST_PROMPTS.ACTION, expected: 'reject' },
|
||||||
|
{ promptName: 'non-existent-prompt', expected: 'reject' },
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
for (const data of actionUpdates) {
|
|
||||||
await assertUpdateThrows(
|
const updateResults = [];
|
||||||
t,
|
for (const { sessionId: testSessionId, updates } of updateTestCases) {
|
||||||
actionSessionId,
|
for (const update of updates) {
|
||||||
data,
|
const { expected: _, ...updateData } = update;
|
||||||
`action session should reject update: ${JSON.stringify(data)}`
|
try {
|
||||||
);
|
await t.context.copilotSession.update({
|
||||||
|
...updateData,
|
||||||
|
userId: user.id,
|
||||||
|
sessionId: testSessionId,
|
||||||
|
});
|
||||||
|
updateResults.push({
|
||||||
|
sessionType:
|
||||||
|
testSessionId === actionSessionId
|
||||||
|
? 'action'
|
||||||
|
: testSessionId === forkedSessionId
|
||||||
|
? 'forked'
|
||||||
|
: 'regular',
|
||||||
|
update: updateData,
|
||||||
|
result: 'success',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
updateResults.push({
|
||||||
|
sessionType:
|
||||||
|
testSessionId === actionSessionId
|
||||||
|
? 'action'
|
||||||
|
: testSessionId === forkedSessionId
|
||||||
|
? 'forked'
|
||||||
|
: 'regular',
|
||||||
|
update: updateData,
|
||||||
|
result:
|
||||||
|
error instanceof CopilotSessionInvalidInput ? 'rejected' : 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// case 2: forked sessions should reject docId updates but allow others
|
t.snapshot(updateResults, 'session update validation results');
|
||||||
{
|
|
||||||
await assertUpdate(
|
|
||||||
t,
|
|
||||||
forkedSessionId,
|
|
||||||
{ pinned: true },
|
|
||||||
'forked session should allow pinned update'
|
|
||||||
);
|
|
||||||
await assertUpdate(
|
|
||||||
t,
|
|
||||||
forkedSessionId,
|
|
||||||
{ promptName: 'test-prompt' },
|
|
||||||
'forked session should allow promptName update'
|
|
||||||
);
|
|
||||||
await assertUpdateThrows(
|
|
||||||
t,
|
|
||||||
forkedSessionId,
|
|
||||||
{ docId: 'new-doc' },
|
|
||||||
'forked session should reject docId update'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// case 3: prompt update validation
|
|
||||||
await assertUpdate(
|
|
||||||
t,
|
|
||||||
sessionId,
|
|
||||||
{ promptName: 'test-prompt' },
|
|
||||||
'should allow valid non-action prompt'
|
|
||||||
);
|
|
||||||
await assertUpdateThrows(
|
|
||||||
t,
|
|
||||||
sessionId,
|
|
||||||
{ promptName: 'action-prompt' },
|
|
||||||
'should reject action prompt'
|
|
||||||
);
|
|
||||||
await assertUpdateThrows(
|
|
||||||
t,
|
|
||||||
sessionId,
|
|
||||||
{ promptName: 'non-existent-prompt' },
|
|
||||||
'should reject non-existent prompt'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// cest 4: session type conversions and pinning behavior
|
// session type conversions
|
||||||
{
|
const existingPinnedId = randomUUID();
|
||||||
const existingPinnedId = 'existing-pinned-session-id';
|
|
||||||
await createTestSession(t, { sessionId: existingPinnedId, pinned: true });
|
await createTestSession(t, { sessionId: existingPinnedId, pinned: true });
|
||||||
|
|
||||||
// should unpin existing when pinning new session
|
|
||||||
await copilotSession.update({ userId: user.id, sessionId, pinned: true });
|
await copilotSession.update({ userId: user.id, sessionId, pinned: true });
|
||||||
|
|
||||||
t.snapshot(
|
// pinning behavior
|
||||||
[
|
const states = await getSessionStates(db, [sessionId, existingPinnedId]);
|
||||||
await getSessionState(db, sessionId),
|
const pinnedCount = states.filter(s => s?.pinned).length;
|
||||||
await getSessionState(db, existingPinnedId),
|
const unpinnedCount = states.filter(s => s && !s.pinned).length;
|
||||||
],
|
|
||||||
'should unpin existing when pinning new session'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// test type conversions
|
t.snapshot(
|
||||||
{
|
{
|
||||||
const conversionSteps: any[] = [];
|
totalSessions: states.length,
|
||||||
const convertSession = async (step: string, data: UpdateData) => {
|
pinnedSessions: pinnedCount,
|
||||||
await copilotSession.update({ ...data, userId: user.id, sessionId });
|
unpinnedSessions: unpinnedCount,
|
||||||
|
onlyOneSessionPinned: pinnedCount === 1,
|
||||||
|
},
|
||||||
|
'pinning behavior - should unpin existing when pinning new'
|
||||||
|
);
|
||||||
|
|
||||||
|
// type conversions
|
||||||
|
const conversionSteps = [];
|
||||||
|
const conversions: Array<[string, UpdateData]> = [
|
||||||
|
['workspace_to_doc', { docId, pinned: false }],
|
||||||
|
['doc_to_workspace', { docId: null }],
|
||||||
|
['workspace_to_pinned', { pinned: true }],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [step, data] of conversions) {
|
||||||
|
await copilotSession.update({ userId: user.id, sessionId, ...data });
|
||||||
const session = await db.aiSession.findUnique({
|
const session = await db.aiSession.findUnique({
|
||||||
where: { id: sessionId },
|
where: { id: sessionId },
|
||||||
select: { docId: true, pinned: true },
|
select: { docId: true, pinned: true },
|
||||||
});
|
});
|
||||||
conversionSteps.push({
|
conversionSteps.push({
|
||||||
step,
|
step,
|
||||||
session,
|
sessionState: {
|
||||||
|
hasDocId: !!session?.docId,
|
||||||
|
pinned: !!session?.pinned,
|
||||||
|
},
|
||||||
type: copilotSession.getSessionType(session!),
|
type: copilotSession.getSessionType(session!),
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const conversions = [
|
|
||||||
['pinned_to_doc', { docId, pinned: false }],
|
|
||||||
['doc_to_workspace', { docId: null }],
|
|
||||||
['workspace_to_pinned', { pinned: true }],
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
for (const [step, data] of conversions) {
|
|
||||||
await convertSession(step, data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t.snapshot(conversionSteps, 'session type conversion steps');
|
t.snapshot(conversionSteps, 'session type conversion steps');
|
||||||
}
|
});
|
||||||
|
|
||||||
|
test('should handle session queries, ordering, and filtering', async t => {
|
||||||
|
const { copilotSession, db } = t.context;
|
||||||
|
await createTestPrompts(copilotSession, db);
|
||||||
|
|
||||||
|
const docId = randomUUID();
|
||||||
|
const sessionIds: string[] = [];
|
||||||
|
const sessionConfigs = [
|
||||||
|
{ type: 'workspace', config: { docId: null, pinned: false } },
|
||||||
|
{ type: 'pinned', config: { docId: null, pinned: true } },
|
||||||
|
{ type: 'doc', config: { docId, pinned: false }, withMessages: true },
|
||||||
|
{
|
||||||
|
type: 'action',
|
||||||
|
config: { docId, promptName: TEST_PROMPTS.ACTION, promptAction: 'edit' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// create sessions with timing delays for ordering tests
|
||||||
|
for (let i = 0; i < sessionConfigs.length; i++) {
|
||||||
|
const { config, withMessages } = sessionConfigs[i];
|
||||||
|
const sessionId = randomUUID();
|
||||||
|
sessionIds.push(sessionId);
|
||||||
|
|
||||||
|
if (withMessages) {
|
||||||
|
await createSessionWithMessages(
|
||||||
|
t,
|
||||||
|
{ sessionId, ...config },
|
||||||
|
`Message for session ${i}`,
|
||||||
|
100 * i
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await createTestSession(t, { sessionId, ...config });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create additional doc sessions for multiple doc test
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
const sessionId = randomUUID();
|
||||||
|
sessionIds.push(sessionId);
|
||||||
|
await createSessionWithMessages(
|
||||||
|
t,
|
||||||
|
{ sessionId, docId },
|
||||||
|
`Additional doc message ${i}`,
|
||||||
|
200 + 100 * i
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create fork session
|
||||||
|
const parentSessionId = sessionIds[2]; // use first doc session as parent
|
||||||
|
const forkedSessionId = randomUUID();
|
||||||
|
await db.aiSession.create({
|
||||||
|
data: {
|
||||||
|
id: forkedSessionId,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
userId: user.id,
|
||||||
|
docId,
|
||||||
|
pinned: false,
|
||||||
|
promptName: TEST_PROMPTS.NORMAL,
|
||||||
|
promptAction: null,
|
||||||
|
parentSessionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseParams = { userId: user.id, workspaceId: workspace.id };
|
||||||
|
const docParams = { ...baseParams, docId };
|
||||||
|
const queryTestCases = [
|
||||||
|
{ name: 'all_workspace_sessions', params: baseParams },
|
||||||
|
{
|
||||||
|
name: 'doc_sessions_with_messages',
|
||||||
|
params: { ...docParams, withMessages: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'recent_top3_sessions',
|
||||||
|
params: { ...baseParams, limit: 3, sessionOrder: 'desc' as const },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'non_action_sessions',
|
||||||
|
params: { ...docParams, action: false },
|
||||||
|
},
|
||||||
|
{ name: 'non_fork_sessions', params: { ...docParams, fork: false } },
|
||||||
|
{
|
||||||
|
name: 'latest_valid_session',
|
||||||
|
params: {
|
||||||
|
...docParams,
|
||||||
|
limit: 1,
|
||||||
|
sessionOrder: 'desc' as const,
|
||||||
|
action: false,
|
||||||
|
fork: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const queryResults: Record<string, any> = {};
|
||||||
|
for (const { name, params } of queryTestCases) {
|
||||||
|
const sessions = await copilotSession.list(params);
|
||||||
|
queryResults[name] = {
|
||||||
|
count: sessions.length,
|
||||||
|
sessionTypes: sessions.map(s => ({
|
||||||
|
type: copilotSession.getSessionType(s),
|
||||||
|
hasMessages: !!s.messages?.length,
|
||||||
|
messageCount: s.messages?.length || 0,
|
||||||
|
isFork: !!s.parentSessionId,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
t.snapshot(queryResults, 'comprehensive session query results');
|
||||||
|
|
||||||
|
// should list sessions appear in correct order
|
||||||
|
{
|
||||||
|
const docSessionsWithMessages = await copilotSession.list({
|
||||||
|
userId: user.id,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
docId,
|
||||||
|
withMessages: true,
|
||||||
|
sessionOrder: 'desc',
|
||||||
|
});
|
||||||
|
|
||||||
|
// check sessions are returned in desc order by updatedAt
|
||||||
|
if (docSessionsWithMessages.length > 1) {
|
||||||
|
for (let i = 1; i < docSessionsWithMessages.length; i++) {
|
||||||
|
const currentSession = docSessionsWithMessages[i - 1];
|
||||||
|
const nextSession = docSessionsWithMessages[i];
|
||||||
|
t.true(
|
||||||
|
currentSession.updatedAt >= nextSession.updatedAt,
|
||||||
|
`sessions should be ordered by updatedAt desc: ${currentSession.updatedAt} >= ${nextSession.updatedAt}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// should update `updatedAt` when updating messages
|
||||||
|
{
|
||||||
|
const oldestDocSession = await copilotSession.list({
|
||||||
|
userId: user.id,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
docId,
|
||||||
|
sessionOrder: 'asc',
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (oldestDocSession.length > 0) {
|
||||||
|
const sessionId = oldestDocSession[0].id;
|
||||||
|
|
||||||
|
// get initial updatedAt
|
||||||
|
const sessionBeforeUpdate = await db.aiSession.findUnique({
|
||||||
|
where: { id: sessionId },
|
||||||
|
select: { updatedAt: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
await addMessagesToSession(
|
||||||
|
copilotSession,
|
||||||
|
sessionId,
|
||||||
|
'Update to verify sorting'
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionAfterUpdate = await db.aiSession.findUnique({
|
||||||
|
where: { id: sessionId },
|
||||||
|
select: { updatedAt: true },
|
||||||
|
});
|
||||||
|
t.true(
|
||||||
|
sessionAfterUpdate!.updatedAt > sessionBeforeUpdate!.updatedAt,
|
||||||
|
'updatedAt should be updated after adding messages'
|
||||||
|
);
|
||||||
|
|
||||||
|
// the updated session now should appears first in desc order
|
||||||
|
const sessionsAfterUpdate = await copilotSession.list({
|
||||||
|
userId: user.id,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
docId,
|
||||||
|
sessionOrder: 'desc',
|
||||||
|
});
|
||||||
|
t.is(
|
||||||
|
sessionsAfterUpdate[0].id,
|
||||||
|
sessionId,
|
||||||
|
'session with updated messages should appear first in descending order'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// should get latest valid session
|
||||||
|
{
|
||||||
|
const latestValidSessions = await copilotSession.list({
|
||||||
|
userId: user.id,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
docId,
|
||||||
|
limit: 1,
|
||||||
|
sessionOrder: 'desc',
|
||||||
|
action: false,
|
||||||
|
fork: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (latestValidSessions.length > 0) {
|
||||||
|
const latestSession = latestValidSessions[0];
|
||||||
|
|
||||||
|
// verify this is indeed a non-action, non-fork session
|
||||||
|
t.falsy(
|
||||||
|
latestSession.parentSessionId,
|
||||||
|
'latest session should not be a fork'
|
||||||
|
);
|
||||||
|
t.not(
|
||||||
|
latestSession.promptName,
|
||||||
|
TEST_PROMPTS.ACTION,
|
||||||
|
'latest session should not use action prompt'
|
||||||
|
);
|
||||||
|
|
||||||
|
// verify it's the most recently updated among valid sessions
|
||||||
|
const allValidSessions = await copilotSession.list({
|
||||||
|
userId: user.id,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
docId,
|
||||||
|
action: false,
|
||||||
|
fork: false,
|
||||||
|
sessionOrder: 'desc',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allValidSessions.length > 0) {
|
||||||
|
t.is(
|
||||||
|
allValidSessions[0].id,
|
||||||
|
latestSession.id,
|
||||||
|
'latest valid session should be the first in the ordered list'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// session type identification
|
||||||
|
const sessionTypeTests = [
|
||||||
|
{ docId: null, pinned: false },
|
||||||
|
{ docId: undefined, pinned: false },
|
||||||
|
{ docId: null, pinned: true },
|
||||||
|
{ docId: 'test-doc-id', pinned: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const sessionTypeResults = sessionTypeTests.map(session => ({
|
||||||
|
session,
|
||||||
|
type: copilotSession.getSessionType(session),
|
||||||
|
}));
|
||||||
|
|
||||||
|
t.snapshot(sessionTypeResults, 'session type identification results');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle fork and session attachment operations', async t => {
|
||||||
|
const { copilotSession } = t.context;
|
||||||
|
await createTestPrompts(copilotSession, t.context.db);
|
||||||
|
|
||||||
|
const parentSessionId = randomUUID();
|
||||||
|
const docId = randomUUID();
|
||||||
|
|
||||||
|
await createSessionWithMessages(
|
||||||
|
t,
|
||||||
|
{ sessionId: parentSessionId, docId },
|
||||||
|
'Original message'
|
||||||
|
);
|
||||||
|
|
||||||
|
const forkTestCases = [
|
||||||
|
{
|
||||||
|
sessionId: randomUUID(),
|
||||||
|
docId: null,
|
||||||
|
pinned: false,
|
||||||
|
description: 'workspace fork',
|
||||||
|
},
|
||||||
|
{ sessionId: randomUUID(), docId, pinned: false, description: 'doc fork' },
|
||||||
|
{
|
||||||
|
sessionId: randomUUID(),
|
||||||
|
docId: null,
|
||||||
|
pinned: true,
|
||||||
|
description: 'pinned fork',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// test unpinning behavior
|
||||||
|
const existingPinnedId = randomUUID();
|
||||||
|
await createTestSession(t, { sessionId: existingPinnedId, pinned: true });
|
||||||
|
|
||||||
|
const performForkOperation = async (
|
||||||
|
copilotSession: CopilotSessionModel,
|
||||||
|
parentSessionId: string,
|
||||||
|
forkConfig: {
|
||||||
|
sessionId: string;
|
||||||
|
docId: string | null;
|
||||||
|
pinned: boolean;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
return await copilotSession.fork({
|
||||||
|
sessionId: forkConfig.sessionId,
|
||||||
|
userId: user.id,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
docId: forkConfig.docId,
|
||||||
|
pinned: forkConfig.pinned,
|
||||||
|
parentSessionId,
|
||||||
|
prompt: { name: TEST_PROMPTS.NORMAL, action: null, model: 'gpt-4.1' },
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: 'Original message',
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// fork operations
|
||||||
|
const forkResults = await Promise.all(
|
||||||
|
forkTestCases.map(async test => {
|
||||||
|
const returnedId = await performForkOperation(
|
||||||
|
copilotSession,
|
||||||
|
parentSessionId,
|
||||||
|
test
|
||||||
|
);
|
||||||
|
const forkedSession = await copilotSession.get(test.sessionId);
|
||||||
|
return {
|
||||||
|
description: test.description,
|
||||||
|
success: returnedId === test.sessionId,
|
||||||
|
actualState: forkedSession
|
||||||
|
? {
|
||||||
|
hasDocId: !!forkedSession.docId,
|
||||||
|
isDocIdCorrect: forkedSession.docId === test.docId,
|
||||||
|
pinned: forkedSession.pinned,
|
||||||
|
hasParent: !!forkedSession.parentSessionId,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// check if pinned fork unpinned existing session
|
||||||
|
const originalPinned = await copilotSession.get(existingPinnedId);
|
||||||
|
|
||||||
|
t.snapshot(
|
||||||
|
{
|
||||||
|
forkResults,
|
||||||
|
existingPinnedSessionUnpinned: !originalPinned?.pinned,
|
||||||
|
},
|
||||||
|
'fork operation results'
|
||||||
|
);
|
||||||
|
|
||||||
|
// attach/detach operations
|
||||||
|
const workspaceSessionId = randomUUID();
|
||||||
|
const existingDocSessionId = randomUUID();
|
||||||
|
const attachTestDocId = randomUUID();
|
||||||
|
|
||||||
|
// sessions for attach/detach test
|
||||||
|
await createTestSession(t, { sessionId: workspaceSessionId, docId: null });
|
||||||
|
await createTestSession(t, {
|
||||||
|
sessionId: existingDocSessionId,
|
||||||
|
docId: attachTestDocId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// attach: workspace -> doc
|
||||||
|
await copilotSession.update({
|
||||||
|
userId: user.id,
|
||||||
|
sessionId: workspaceSessionId,
|
||||||
|
docId: attachTestDocId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const docSessionsAfterAttach = await copilotSession.list({
|
||||||
|
userId: user.id,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
docId: attachTestDocId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// detach: doc -> workspace
|
||||||
|
await copilotSession.update({
|
||||||
|
userId: user.id,
|
||||||
|
sessionId: workspaceSessionId,
|
||||||
|
docId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspaceSessionsAfterDetach = await copilotSession.list({
|
||||||
|
userId: user.id,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
docId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const remainingDocSessions = await copilotSession.list({
|
||||||
|
userId: user.id,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
docId: attachTestDocId,
|
||||||
|
});
|
||||||
|
|
||||||
|
t.snapshot(
|
||||||
|
{
|
||||||
|
attachPhase: {
|
||||||
|
docSessionCount: docSessionsAfterAttach.length,
|
||||||
|
bothSessionsPresent:
|
||||||
|
docSessionsAfterAttach.some(s => s.id === workspaceSessionId) &&
|
||||||
|
docSessionsAfterAttach.some(s => s.id === existingDocSessionId),
|
||||||
|
},
|
||||||
|
detachPhase: {
|
||||||
|
workspaceSessionExists: workspaceSessionsAfterDetach.some(
|
||||||
|
s => s.id === workspaceSessionId && !s.pinned
|
||||||
|
),
|
||||||
|
originalDocSessionRemains:
|
||||||
|
remainingDocSessions.length === 1 &&
|
||||||
|
remainingDocSessions[0].id === existingDocSessionId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'attach and detach operation results'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
@ -206,6 +206,7 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
// save message
|
// save message
|
||||||
await this.models.copilotSession.updateMessages({
|
await this.models.copilotSession.updateMessages({
|
||||||
...forkedState,
|
...forkedState,
|
||||||
|
sessionId,
|
||||||
messages,
|
messages,
|
||||||
});
|
});
|
||||||
return sessionId;
|
return sessionId;
|
||||||
@ -286,18 +287,37 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
async list(options: ListSessionOptions) {
|
async list(options: ListSessionOptions) {
|
||||||
const { userId, sessionId, workspaceId, docId } = options;
|
const { userId, sessionId, workspaceId, docId } = options;
|
||||||
|
|
||||||
const extraCondition = [];
|
const conditions: Prisma.AiSessionWhereInput['OR'] = [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
docId: docId ?? null,
|
||||||
|
id: sessionId ? { equals: sessionId } : undefined,
|
||||||
|
deletedAt: null,
|
||||||
|
prompt:
|
||||||
|
typeof options.action === 'boolean'
|
||||||
|
? options.action
|
||||||
|
? { action: { not: null } }
|
||||||
|
: { action: null }
|
||||||
|
: undefined,
|
||||||
|
parentSessionId:
|
||||||
|
typeof options.fork === 'boolean'
|
||||||
|
? options.fork
|
||||||
|
? { not: null }
|
||||||
|
: null
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
if (!options?.action && options?.fork) {
|
if (!options?.action && options?.fork) {
|
||||||
|
// query forked sessions from other users
|
||||||
// only query forked session if fork == true and action == false
|
// only query forked session if fork == true and action == false
|
||||||
extraCondition.push({
|
conditions.push({
|
||||||
userId: { not: userId },
|
userId: { not: userId },
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
docId: docId ?? null,
|
docId: docId ?? null,
|
||||||
id: sessionId ? { equals: sessionId } : undefined,
|
id: sessionId ? { equals: sessionId } : undefined,
|
||||||
prompt: {
|
prompt: { action: null },
|
||||||
action: options.action ? { not: null } : null,
|
|
||||||
},
|
|
||||||
// should only find forked session
|
// should only find forked session
|
||||||
parentSessionId: { not: null },
|
parentSessionId: { not: null },
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
@ -305,18 +325,7 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await this.db.aiSession.findMany({
|
return await this.db.aiSession.findMany({
|
||||||
where: {
|
where: { OR: conditions },
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
workspaceId,
|
|
||||||
docId: docId ?? null,
|
|
||||||
id: sessionId ? { equals: sessionId } : undefined,
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
...extraCondition,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
@ -327,6 +336,7 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
promptName: true,
|
promptName: true,
|
||||||
tokenCost: true,
|
tokenCost: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
messages: options.withMessages
|
messages: options.withMessages
|
||||||
? {
|
? {
|
||||||
select: {
|
select: {
|
||||||
@ -348,8 +358,7 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
take: options?.limit,
|
take: options?.limit,
|
||||||
skip: options?.skip,
|
skip: options?.skip,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
// session order is desc by default
|
updatedAt: options?.sessionOrder === 'asc' ? 'asc' : 'desc',
|
||||||
createdAt: options?.sessionOrder === 'asc' ? 'asc' : 'desc',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -235,6 +235,12 @@ class CopilotHistoriesType implements Partial<ChatHistory> {
|
|||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
sessionId!: string;
|
sessionId!: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
workspaceId!: string;
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
docId!: string | null;
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
pinned!: boolean;
|
pinned!: boolean;
|
||||||
|
|
||||||
@ -254,6 +260,9 @@ class CopilotHistoriesType implements Partial<ChatHistory> {
|
|||||||
|
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@Field(() => Date)
|
||||||
|
updatedAt!: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObjectType('CopilotQuota')
|
@ObjectType('CopilotQuota')
|
||||||
|
@ -298,13 +298,16 @@ export class ChatSessionService {
|
|||||||
const histories = await Promise.all(
|
const histories = await Promise.all(
|
||||||
sessions.map(
|
sessions.map(
|
||||||
async ({
|
async ({
|
||||||
id,
|
|
||||||
userId: uid,
|
userId: uid,
|
||||||
|
id,
|
||||||
|
workspaceId,
|
||||||
|
docId,
|
||||||
pinned,
|
pinned,
|
||||||
promptName,
|
promptName,
|
||||||
tokenCost,
|
tokenCost,
|
||||||
messages,
|
messages,
|
||||||
createdAt,
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const prompt = await this.prompt.get(promptName);
|
const prompt = await this.prompt.get(promptName);
|
||||||
@ -341,10 +344,13 @@ export class ChatSessionService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
sessionId: id,
|
sessionId: id,
|
||||||
|
workspaceId,
|
||||||
|
docId,
|
||||||
pinned,
|
pinned,
|
||||||
action: prompt.action || null,
|
action: prompt.action || null,
|
||||||
tokens: tokenCost,
|
tokens: tokenCost,
|
||||||
createdAt,
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
messages: preload.concat(ret.data).map(m => ({
|
messages: preload.concat(ret.data).map(m => ({
|
||||||
...m,
|
...m,
|
||||||
attachments: m.attachments
|
attachments: m.attachments
|
||||||
|
@ -47,11 +47,14 @@ export type ChatMessage = z.infer<typeof ChatMessageSchema>;
|
|||||||
export const ChatHistorySchema = z
|
export const ChatHistorySchema = z
|
||||||
.object({
|
.object({
|
||||||
sessionId: z.string(),
|
sessionId: z.string(),
|
||||||
|
workspaceId: z.string(),
|
||||||
|
docId: z.string().nullable(),
|
||||||
pinned: z.boolean(),
|
pinned: z.boolean(),
|
||||||
action: z.string().nullable(),
|
action: z.string().nullable(),
|
||||||
tokens: z.number(),
|
tokens: z.number(),
|
||||||
messages: z.array(ChatMessageSchema),
|
messages: z.array(ChatMessageSchema),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
@ -237,12 +237,15 @@ type CopilotHistories {
|
|||||||
"""An mark identifying which view to use to display the session"""
|
"""An mark identifying which view to use to display the session"""
|
||||||
action: String
|
action: String
|
||||||
createdAt: DateTime!
|
createdAt: DateTime!
|
||||||
|
docId: String
|
||||||
messages: [ChatMessage!]!
|
messages: [ChatMessage!]!
|
||||||
pinned: Boolean!
|
pinned: Boolean!
|
||||||
sessionId: String!
|
sessionId: String!
|
||||||
|
|
||||||
"""The number of tokens used in the session"""
|
"""The number of tokens used in the session"""
|
||||||
tokens: Int!
|
tokens: Int!
|
||||||
|
updatedAt: DateTime!
|
||||||
|
workspaceId: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type CopilotInvalidContextDataType {
|
type CopilotInvalidContextDataType {
|
||||||
@ -253,22 +256,6 @@ type CopilotMessageNotFoundDataType {
|
|||||||
messageId: String!
|
messageId: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CopilotModels {
|
|
||||||
DallE3
|
|
||||||
Gpt4Omni
|
|
||||||
Gpt4Omni0806
|
|
||||||
Gpt4OmniMini
|
|
||||||
Gpt4OmniMini0718
|
|
||||||
Gpt41
|
|
||||||
Gpt41Mini
|
|
||||||
Gpt41Nano
|
|
||||||
Gpt410414
|
|
||||||
GptImage
|
|
||||||
TextEmbedding3Large
|
|
||||||
TextEmbedding3Small
|
|
||||||
TextEmbeddingAda002
|
|
||||||
}
|
|
||||||
|
|
||||||
input CopilotPromptConfigInput {
|
input CopilotPromptConfigInput {
|
||||||
frequencyPenalty: Float
|
frequencyPenalty: Float
|
||||||
presencePenalty: Float
|
presencePenalty: Float
|
||||||
@ -408,7 +395,7 @@ input CreateCopilotPromptInput {
|
|||||||
action: String
|
action: String
|
||||||
config: CopilotPromptConfigInput
|
config: CopilotPromptConfigInput
|
||||||
messages: [CopilotPromptMessageInput!]!
|
messages: [CopilotPromptMessageInput!]!
|
||||||
model: CopilotModels!
|
model: String!
|
||||||
name: String!
|
name: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
query getCopilotLatestDocSession(
|
||||||
|
$workspaceId: String!
|
||||||
|
$docId: String!
|
||||||
|
) {
|
||||||
|
currentUser {
|
||||||
|
copilot(workspaceId: $workspaceId) {
|
||||||
|
histories(
|
||||||
|
docId: $docId
|
||||||
|
options: {
|
||||||
|
limit: 1
|
||||||
|
sessionOrder: desc
|
||||||
|
action: false
|
||||||
|
fork: false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
sessionId
|
||||||
|
workspaceId
|
||||||
|
docId
|
||||||
|
pinned
|
||||||
|
action
|
||||||
|
tokens
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
messages {
|
||||||
|
id
|
||||||
|
role
|
||||||
|
content
|
||||||
|
attachments
|
||||||
|
params
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
query getCopilotRecentSessions(
|
||||||
|
$workspaceId: String!
|
||||||
|
$limit: Int = 10
|
||||||
|
) {
|
||||||
|
currentUser {
|
||||||
|
copilot(workspaceId: $workspaceId) {
|
||||||
|
histories(
|
||||||
|
options: {
|
||||||
|
limit: $limit
|
||||||
|
sessionOrder: desc
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
sessionId
|
||||||
|
workspaceId
|
||||||
|
docId
|
||||||
|
pinned
|
||||||
|
action
|
||||||
|
tokens
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -755,6 +755,38 @@ export const forkCopilotSessionMutation = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCopilotLatestDocSessionQuery = {
|
||||||
|
id: 'getCopilotLatestDocSessionQuery' as const,
|
||||||
|
op: 'getCopilotLatestDocSession',
|
||||||
|
query: `query getCopilotLatestDocSession($workspaceId: String!, $docId: String!) {
|
||||||
|
currentUser {
|
||||||
|
copilot(workspaceId: $workspaceId) {
|
||||||
|
histories(
|
||||||
|
docId: $docId
|
||||||
|
options: {limit: 1, sessionOrder: desc, action: false, fork: false}
|
||||||
|
) {
|
||||||
|
sessionId
|
||||||
|
workspaceId
|
||||||
|
docId
|
||||||
|
pinned
|
||||||
|
action
|
||||||
|
tokens
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
messages {
|
||||||
|
id
|
||||||
|
role
|
||||||
|
content
|
||||||
|
attachments
|
||||||
|
params
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const getCopilotSessionQuery = {
|
export const getCopilotSessionQuery = {
|
||||||
id: 'getCopilotSessionQuery' as const,
|
id: 'getCopilotSessionQuery' as const,
|
||||||
op: 'getCopilotSession',
|
op: 'getCopilotSession',
|
||||||
@ -775,6 +807,27 @@ export const getCopilotSessionQuery = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCopilotRecentSessionsQuery = {
|
||||||
|
id: 'getCopilotRecentSessionsQuery' as const,
|
||||||
|
op: 'getCopilotRecentSessions',
|
||||||
|
query: `query getCopilotRecentSessions($workspaceId: String!, $limit: Int = 10) {
|
||||||
|
currentUser {
|
||||||
|
copilot(workspaceId: $workspaceId) {
|
||||||
|
histories(options: {limit: $limit, sessionOrder: desc}) {
|
||||||
|
sessionId
|
||||||
|
workspaceId
|
||||||
|
docId
|
||||||
|
pinned
|
||||||
|
action
|
||||||
|
tokens
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const updateCopilotSessionMutation = {
|
export const updateCopilotSessionMutation = {
|
||||||
id: 'updateCopilotSessionMutation' as const,
|
id: 'updateCopilotSessionMutation' as const,
|
||||||
op: 'updateCopilotSession',
|
op: 'updateCopilotSession',
|
||||||
|
@ -323,11 +323,14 @@ export interface CopilotHistories {
|
|||||||
/** An mark identifying which view to use to display the session */
|
/** An mark identifying which view to use to display the session */
|
||||||
action: Maybe<Scalars['String']['output']>;
|
action: Maybe<Scalars['String']['output']>;
|
||||||
createdAt: Scalars['DateTime']['output'];
|
createdAt: Scalars['DateTime']['output'];
|
||||||
|
docId: Maybe<Scalars['String']['output']>;
|
||||||
messages: Array<ChatMessage>;
|
messages: Array<ChatMessage>;
|
||||||
pinned: Scalars['Boolean']['output'];
|
pinned: Scalars['Boolean']['output'];
|
||||||
sessionId: Scalars['String']['output'];
|
sessionId: Scalars['String']['output'];
|
||||||
/** The number of tokens used in the session */
|
/** The number of tokens used in the session */
|
||||||
tokens: Scalars['Int']['output'];
|
tokens: Scalars['Int']['output'];
|
||||||
|
updatedAt: Scalars['DateTime']['output'];
|
||||||
|
workspaceId: Scalars['String']['output'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CopilotInvalidContextDataType {
|
export interface CopilotInvalidContextDataType {
|
||||||
@ -340,22 +343,6 @@ export interface CopilotMessageNotFoundDataType {
|
|||||||
messageId: Scalars['String']['output'];
|
messageId: Scalars['String']['output'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CopilotModels {
|
|
||||||
DallE3 = 'DallE3',
|
|
||||||
Gpt4Omni = 'Gpt4Omni',
|
|
||||||
Gpt4Omni0806 = 'Gpt4Omni0806',
|
|
||||||
Gpt4OmniMini = 'Gpt4OmniMini',
|
|
||||||
Gpt4OmniMini0718 = 'Gpt4OmniMini0718',
|
|
||||||
Gpt41 = 'Gpt41',
|
|
||||||
Gpt41Mini = 'Gpt41Mini',
|
|
||||||
Gpt41Nano = 'Gpt41Nano',
|
|
||||||
Gpt410414 = 'Gpt410414',
|
|
||||||
GptImage = 'GptImage',
|
|
||||||
TextEmbedding3Large = 'TextEmbedding3Large',
|
|
||||||
TextEmbedding3Small = 'TextEmbedding3Small',
|
|
||||||
TextEmbeddingAda002 = 'TextEmbeddingAda002',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CopilotPromptConfigInput {
|
export interface CopilotPromptConfigInput {
|
||||||
frequencyPenalty?: InputMaybe<Scalars['Float']['input']>;
|
frequencyPenalty?: InputMaybe<Scalars['Float']['input']>;
|
||||||
presencePenalty?: InputMaybe<Scalars['Float']['input']>;
|
presencePenalty?: InputMaybe<Scalars['Float']['input']>;
|
||||||
@ -515,7 +502,7 @@ export interface CreateCopilotPromptInput {
|
|||||||
action?: InputMaybe<Scalars['String']['input']>;
|
action?: InputMaybe<Scalars['String']['input']>;
|
||||||
config?: InputMaybe<CopilotPromptConfigInput>;
|
config?: InputMaybe<CopilotPromptConfigInput>;
|
||||||
messages: Array<CopilotPromptMessageInput>;
|
messages: Array<CopilotPromptMessageInput>;
|
||||||
model: CopilotModels;
|
model: Scalars['String']['input'];
|
||||||
name: Scalars['String']['input'];
|
name: Scalars['String']['input'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3575,6 +3562,41 @@ export type ForkCopilotSessionMutation = {
|
|||||||
forkCopilotSession: string;
|
forkCopilotSession: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetCopilotLatestDocSessionQueryVariables = Exact<{
|
||||||
|
workspaceId: Scalars['String']['input'];
|
||||||
|
docId: Scalars['String']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetCopilotLatestDocSessionQuery = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
currentUser: {
|
||||||
|
__typename?: 'UserType';
|
||||||
|
copilot: {
|
||||||
|
__typename?: 'Copilot';
|
||||||
|
histories: Array<{
|
||||||
|
__typename?: 'CopilotHistories';
|
||||||
|
sessionId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
docId: string | null;
|
||||||
|
pinned: boolean;
|
||||||
|
action: string | null;
|
||||||
|
tokens: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
messages: Array<{
|
||||||
|
__typename?: 'ChatMessage';
|
||||||
|
id: string | null;
|
||||||
|
role: string;
|
||||||
|
content: string;
|
||||||
|
attachments: Array<string> | null;
|
||||||
|
params: Record<string, string> | null;
|
||||||
|
createdAt: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type GetCopilotSessionQueryVariables = Exact<{
|
export type GetCopilotSessionQueryVariables = Exact<{
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
sessionId: Scalars['String']['input'];
|
sessionId: Scalars['String']['input'];
|
||||||
@ -3600,6 +3622,32 @@ export type GetCopilotSessionQuery = {
|
|||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetCopilotRecentSessionsQueryVariables = Exact<{
|
||||||
|
workspaceId: Scalars['String']['input'];
|
||||||
|
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetCopilotRecentSessionsQuery = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
currentUser: {
|
||||||
|
__typename?: 'UserType';
|
||||||
|
copilot: {
|
||||||
|
__typename?: 'Copilot';
|
||||||
|
histories: Array<{
|
||||||
|
__typename?: 'CopilotHistories';
|
||||||
|
sessionId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
docId: string | null;
|
||||||
|
pinned: boolean;
|
||||||
|
action: string | null;
|
||||||
|
tokens: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type UpdateCopilotSessionMutationVariables = Exact<{
|
export type UpdateCopilotSessionMutationVariables = Exact<{
|
||||||
options: UpdateChatSessionInput;
|
options: UpdateChatSessionInput;
|
||||||
}>;
|
}>;
|
||||||
@ -5209,11 +5257,21 @@ export type Queries =
|
|||||||
variables: CopilotQuotaQueryVariables;
|
variables: CopilotQuotaQueryVariables;
|
||||||
response: CopilotQuotaQuery;
|
response: CopilotQuotaQuery;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'getCopilotLatestDocSessionQuery';
|
||||||
|
variables: GetCopilotLatestDocSessionQueryVariables;
|
||||||
|
response: GetCopilotLatestDocSessionQuery;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'getCopilotSessionQuery';
|
name: 'getCopilotSessionQuery';
|
||||||
variables: GetCopilotSessionQueryVariables;
|
variables: GetCopilotSessionQueryVariables;
|
||||||
response: GetCopilotSessionQuery;
|
response: GetCopilotSessionQuery;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'getCopilotRecentSessionsQuery';
|
||||||
|
variables: GetCopilotRecentSessionsQueryVariables;
|
||||||
|
response: GetCopilotRecentSessionsQuery;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'getCopilotSessionsQuery';
|
name: 'getCopilotSessionsQuery';
|
||||||
variables: GetCopilotSessionsQueryVariables;
|
variables: GetCopilotSessionsQueryVariables;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user