Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions docs/oauth-scopes.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ The `ConversationalAgents` scope is required for real-time WebSocket sessions (`
| `getAll()` | `OR.Execution` or `OR.Execution.Read`, `OR.Jobs` or `OR.Jobs.Read` |
| `getById()` | `OR.Execution` or `OR.Execution.Read`, `OR.Jobs` or `OR.Jobs.Read` |
| `createFeedback()` | `OR.Execution`, `OR.Jobs`, `Traces.Api` |
| `end()` | `OR.Jobs` or `OR.Jobs.Write` |

### Messages

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type {
ConversationAttachmentCreateResponse
} from './conversations.types';
import type { ExchangeServiceModel, ConversationExchangeServiceModel } from './exchanges.models';
import type { ExchangeGetByIdOptions, CreateFeedbackOptions } from './exchanges.types';
import type { ExchangeGetByIdOptions, CreateFeedbackOptions, ExchangeEndResponse } from './exchanges.types';
import type { PaginatedResponse } from '@/utils/pagination';
import type { ConnectionStatus, ConnectionStatusChangedHandler } from '@/core/websocket';
import type { SessionStream } from './types/events/session.types';
Expand Down Expand Up @@ -559,6 +559,11 @@ function createConversationMethods(
if (!conversationData.id) throw new Error('Conversation ID is undefined');
if (!exchangeService) throw new Error('Exchange methods are not available.');
return exchangeService.createFeedback(conversationData.id, exchangeId, options);
},
end(exchangeId: string): Promise<ExchangeEndResponse> {
if (!conversationData.id) throw new Error('Conversation ID is undefined');
if (!exchangeService) throw new Error('Exchange methods are not available.');
return exchangeService.end(conversationData.id, exchangeId);
}
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import type {
ExchangeGetByIdOptions,
CreateFeedbackOptions,
FeedbackCreateResponse,
ExchangeGetResponse
ExchangeGetResponse,
ExchangeEndResponse
} from './exchanges.types';
import type { PaginatedResponse } from '@/utils/pagination';

Expand Down Expand Up @@ -124,6 +125,24 @@ export interface ExchangeServiceModel {
exchangeId: string,
options: CreateFeedbackOptions
): Promise<FeedbackCreateResponse>;

/**
* Ends an exchange, setting the endedAt timestamp and stopping any running agent job
*
* @internal

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are adding the scopes on OAuth lets change this to @experimental

* @param conversationId - The conversation containing the exchange
* @param exchangeId - The exchange to end
* @returns Promise resolving to {@link ExchangeEndResponse}
* @example
* ```typescript
* const endedExchange = await exchanges.end(conversationId, exchangeId);
* console.log(`Exchange ended at: ${endedExchange.endedAt}`);
* ```
*/
end(
conversationId: string,
exchangeId: string
): Promise<ExchangeEndResponse>;
}

/**
Expand Down Expand Up @@ -219,4 +238,19 @@ export interface ConversationExchangeServiceModel {
exchangeId: string,
options: CreateFeedbackOptions
): Promise<FeedbackCreateResponse>;

/**
* Ends an exchange in this conversation
*
* @internal
* @param exchangeId - The exchange to end
* @returns Promise resolving to {@link ExchangeEndResponse}
*
* @example
* ```typescript
* const endedExchange = await conversation.exchanges.end(exchangeId);
* console.log(`Exchange ended at: ${endedExchange.endedAt}`);
* ```
*/
end(exchangeId: string): Promise<ExchangeEndResponse>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export interface ExchangeGetByIdOptions {
messageSort?: SortOrder;
}

export type ExchangeEndResponse = ExchangeGetResponse;
Comment thread
norman-le marked this conversation as resolved.
Outdated

// ==================== Feedback Types ====================

export interface CreateFeedbackOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,10 @@ export interface Exchange {
* Span identifier for distributed tracing.
*/
spanId?: string;
/**
* Timestamp indicating when the exchange ended, if it has ended.
*/
endedAt?: string;
Comment thread
norman-le marked this conversation as resolved.
Outdated
/**
* The optional feedback rating given by the user.
*/
Expand Down
28 changes: 27 additions & 1 deletion src/services/conversational-agent/conversations/exchanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
FeedbackCreateResponse,
ExchangeGetAllOptions,
ExchangeGetByIdOptions,
ExchangeGetResponse
ExchangeGetResponse,
ExchangeEndResponse
} from '@/models/conversational-agent';

// Utils
Expand Down Expand Up @@ -119,7 +120,7 @@
options?: ExchangeGetAllOptions
): Promise<PaginatedResponse<ExchangeGetResponse>> {
const { pageSize, cursor, jumpToPage, ...additionalParams } = options ?? {};
const paginationParams = cursor ? { cursor, pageSize } : jumpToPage ? { jumpToPage, pageSize } : { pageSize };

Check warning on line 123 in src/services/conversational-agent/conversations/exchanges.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-typescript&issues=AZ688bkUveQgd5zn3NRo&open=AZ688bkUveQgd5zn3NRo&pullRequest=510

return PaginationHelpers.getAllPaginated<Exchange, ExchangeGetResponse>({
serviceAccess: this.createPaginationServiceAccess(),
Expand Down Expand Up @@ -217,4 +218,29 @@
);
return response.data;
}

/**
* Ends an exchange, setting the endedAt timestamp and stopping any running agent job
*
* @internal
* @param conversationId - The conversation containing the exchange
* @param exchangeId - The exchange to end
* @returns Promise resolving to {@link ExchangeEndResponse}
*
* @example
* ```typescript
* const endedExchange = await exchanges.end(conversationId, exchangeId);
* console.log(`Exchange ended at: ${endedExchange.endedAt}`);
* ```
*/
@track('ConversationalAgent.Exchanges.End')
async end(
conversationId: string,
exchangeId: string
): Promise<ExchangeEndResponse> {
const response = await this.post<Exchange>(
EXCHANGE_ENDPOINTS.END(conversationId, exchangeId)
);
return transformExchange(response.data);
}
}
4 changes: 3 additions & 1 deletion src/utils/constants/endpoints/conversational-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export const EXCHANGE_ENDPOINTS = {
GET: (conversationId: string, exchangeId: string) =>
`${AUTOPILOT_BASE}/api/${API_VERSION}/conversation/${conversationId}/exchange/${exchangeId}`,
CREATE_FEEDBACK: (conversationId: string, exchangeId: string) =>
`${AUTOPILOT_BASE}/api/${API_VERSION}/conversation/${conversationId}/exchange/${exchangeId}/feedback`
`${AUTOPILOT_BASE}/api/${API_VERSION}/conversation/${conversationId}/exchange/${exchangeId}/feedback`,
END: (conversationId: string, exchangeId: string) =>
`${AUTOPILOT_BASE}/api/${API_VERSION}/conversation/${conversationId}/exchange/${exchangeId}/end`
} as const;

/**
Expand Down
40 changes: 39 additions & 1 deletion tests/unit/models/conversational-agent/conversations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ describe('Conversation Models', () => {
getAll: vi.fn(),
getById: vi.fn(),
createFeedback: vi.fn(),
} as any;
end: vi.fn(),
Comment thread
norman-le marked this conversation as resolved.
Outdated
};
});

afterEach(() => {
Expand Down Expand Up @@ -95,6 +96,7 @@ describe('Conversation Models', () => {
expect(typeof conversation.exchanges.getAll).toBe('function');
expect(typeof conversation.exchanges.getById).toBe('function');
expect(typeof conversation.exchanges.createFeedback).toBe('function');
expect(typeof conversation.exchanges.end).toBe('function');
});
});

Expand Down Expand Up @@ -465,6 +467,42 @@ describe('Conversation Models', () => {
).toThrow('Exchange methods are not available');
});
});

describe('exchanges.end()', () => {
it('should delegate to exchangeService.end', async () => {
const mockResponse = {};
(mockExchangeService.end as any).mockResolvedValue(mockResponse);

const data = createMockConversationData();
const conversation = createConversationWithMethods(data, mockService, mockSessionMethods, mockExchangeService);

const result = await conversation.exchanges.end(CONVERSATIONAL_AGENT_TEST_CONSTANTS.EXCHANGE_ID);

expect(mockExchangeService.end).toHaveBeenCalledWith(
CONVERSATIONAL_AGENT_TEST_CONSTANTS.CONVERSATION_ID,
CONVERSATIONAL_AGENT_TEST_CONSTANTS.EXCHANGE_ID
);
expect(result).toEqual(mockResponse);
});

it('should throw error if conversation id is undefined', () => {
const data = createMockConversationData({ id: undefined });
const conversation = createConversationWithMethods(data, mockService, mockSessionMethods, mockExchangeService);

expect(
() => conversation.exchanges.end(CONVERSATIONAL_AGENT_TEST_CONSTANTS.EXCHANGE_ID)
).toThrow('Conversation ID is undefined');
});

it('should throw error if exchange service is not available', () => {
const data = createMockConversationData();
const conversation = createConversationWithMethods(data, mockService, mockSessionMethods);

expect(
() => conversation.exchanges.end(CONVERSATIONAL_AGENT_TEST_CONSTANTS.EXCHANGE_ID)
).toThrow('Exchange methods are not available');
});
});
});

describe('data isolation', () => {
Expand Down
51 changes: 51 additions & 0 deletions tests/unit/services/conversational-agent/exchanges.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,4 +441,55 @@ describe('ExchangeService Unit Tests', () => {
).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE);
});
});

describe('end', () => {
it('should end an exchange and return transformed result', async () => {
const mockExchange = createMockRawExchange();
mockApiClient.post.mockResolvedValue(mockExchange);
Comment thread
norman-le marked this conversation as resolved.
Outdated

const result = await exchanges.end(
CONVERSATIONAL_AGENT_TEST_CONSTANTS.CONVERSATION_ID,
CONVERSATIONAL_AGENT_TEST_CONSTANTS.EXCHANGE_ID
);

expect(result).toBeDefined();
expect(result.id).toBe(CONVERSATIONAL_AGENT_TEST_CONSTANTS.EXCHANGE_ID);
expect(result.exchangeId).toBe(CONVERSATIONAL_AGENT_TEST_CONSTANTS.EXCHANGE_ID);
Comment thread
norman-le marked this conversation as resolved.
Outdated

expect(mockApiClient.post).toHaveBeenCalledWith(
EXCHANGE_ENDPOINTS.END(
CONVERSATIONAL_AGENT_TEST_CONSTANTS.CONVERSATION_ID,
CONVERSATIONAL_AGENT_TEST_CONSTANTS.EXCHANGE_ID
),
undefined,
expect.any(Object)
);
});

it('should transform messages within the ended exchange', async () => {
const mockExchange = createMockRawExchange();
mockApiClient.post.mockResolvedValue(mockExchange);

const result = await exchanges.end(
CONVERSATIONAL_AGENT_TEST_CONSTANTS.CONVERSATION_ID,
CONVERSATIONAL_AGENT_TEST_CONSTANTS.EXCHANGE_ID
);

expect(result.messages).toHaveLength(2);
expect(result.messages[0].role).toBe('user');
expect(result.messages[1].role).toBe('assistant');
});

it('should handle API errors', async () => {
const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE);
mockApiClient.post.mockRejectedValue(error);

await expect(
exchanges.end(
CONVERSATIONAL_AGENT_TEST_CONSTANTS.CONVERSATION_ID,
CONVERSATIONAL_AGENT_TEST_CONSTANTS.EXCHANGE_ID
)
).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE);
});
});
});
Loading