diff --git a/packages/agent-toolkit/package.json b/packages/agent-toolkit/package.json index 5608d192..aebc60c9 100644 --- a/packages/agent-toolkit/package.json +++ b/packages/agent-toolkit/package.json @@ -51,6 +51,7 @@ "@mondaydotcomorg/api": "^13.0.0", "axios": "^1.10.0", "exceljs": "^4.4.0", + "fast-deep-equal": "^3.1.3", "jsonwebtoken": "^9.0.2", "mammoth": "^1.12.0", "unpdf": "^1.6.0", diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/get-board-activity/get-board-activity-tool.test.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/get-board-activity/get-board-activity-tool.test.ts index a346e8b5..10f1907b 100644 --- a/packages/agent-toolkit/src/core/tools/platform-api-tools/get-board-activity/get-board-activity-tool.test.ts +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/get-board-activity/get-board-activity-tool.test.ts @@ -37,7 +37,7 @@ describe('GetBoardActivityTool', () => { }); }); - it('includes data field only when includeData is true', async () => { + it('includes data field parsed as an object when includeData is true', async () => { mocks.setResponse({ boards: [mockBoard] }); const tool = new GetBoardActivityTool(mocks.mockApiClient); @@ -49,8 +49,8 @@ describe('GetBoardActivityTool', () => { board_name: 'Test Board', board_url: 'https://monday.com/boards/1', data: [ - { created_at: '2024-01-01T00:00:00Z', event: 'create_pulse', entity: 'pulse', user_id: '123', data: '{"key":"value"}' }, - { created_at: '2024-01-02T00:00:00Z', event: 'update_pulse', entity: 'pulse', user_id: '456', data: '{"foo":"bar"}' }, + { created_at: '2024-01-01T00:00:00Z', event: 'create_pulse', entity: 'pulse', user_id: '123', data: { key: 'value' } }, + { created_at: '2024-01-02T00:00:00Z', event: 'update_pulse', entity: 'pulse', user_id: '456', data: { foo: 'bar' } }, ], }); }); @@ -122,6 +122,41 @@ describe('GetBoardActivityTool', () => { ); }); + it('trims redundant previous_value from data payloads when includeData is true', async () => { + const files = [ + { assetId: 1, name: 'a.pdf' }, + { assetId: 2, name: 'b.pdf' }, + ]; + const heavyBoard = { + name: 'Heavy Board', + url: 'https://monday.com/boards/9', + activity_logs: [ + { + created_at: '2024-03-01T00:00:00Z', + event: 'update_column_value', + entity: 'pulse', + user_id: '77', + data: JSON.stringify({ + action_record_uuid: 'uuid-abc', + column_id: 'files', + previous_value: files, + value: files, + }), + }, + ], + }; + mocks.setResponse({ boards: [heavyBoard] }); + const tool = new GetBoardActivityTool(mocks.mockApiClient); + + const result = await tool.execute({ boardId: 9, includeData: true }); + const row = (result.content as { data: Array<{ data: Record }> }).data[0]; + + expect(row.data.action_record_uuid).toBe('uuid-abc'); + expect(row.data.value).toEqual(files); + expect(row.data.previous_value).toBeUndefined(); + expect(row.data.previous_value_omitted).toBe('equals_value'); + }); + it('propagates API errors', async () => { mocks.getMockRequest().mockRejectedValueOnce(new Error('Unauthorized')); const tool = new GetBoardActivityTool(mocks.mockApiClient); diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/get-board-activity/get-board-activity-tool.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/get-board-activity/get-board-activity-tool.ts index 45b05a7b..92868e06 100644 --- a/packages/agent-toolkit/src/core/tools/platform-api-tools/get-board-activity/get-board-activity-tool.ts +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/get-board-activity/get-board-activity-tool.ts @@ -4,6 +4,7 @@ import { GetBoardActivityQueryVariables, } from '../../../../monday-graphql/generated/graphql/graphql'; import { getBoardActivity } from './get-board-activity.graphql'; +import { trimActivityData } from './trim-activity-data'; import { ToolInputType, ToolOutputType, ToolType } from '../../../tool'; import { BaseMondayApiTool, createMondayApiAnnotations } from './../base-monday-api-tool'; import { TIME_IN_MILLISECONDS } from '../../../../utils'; @@ -28,7 +29,7 @@ export const getBoardActivityToolSchema = { .optional() .default(false) .describe( - 'Whether to include the raw data payload for each activity entry. The data field contains the full before/after state of changes and can be very large. Only set to true when you need the detailed change data.', + 'Whether to include the parsed data payload for each activity entry. The data field contains the full before/after state of changes and can be very large. Only set to true when you need the detailed change data. When true, `data` is returned as a parsed object (not a JSON string); if `previous_value` equals `value` in a row, `previous_value` is dropped and `previous_value_omitted: "equals_value"` is added in its place.', ), }; @@ -102,7 +103,7 @@ export class GetBoardActivityTool extends BaseMondayApiTool { + it('returns the parsed object unchanged when data has no previous_value/value pair', () => { + const input = '{"action_record_uuid":"abc-123","key":"value"}'; + expect(trimActivityData(input)).toEqual({ action_record_uuid: 'abc-123', key: 'value' }); + }); + + it('returns the original string when JSON parsing fails', () => { + const input = 'not valid json'; + expect(trimActivityData(input)).toBe(input); + }); + + it('returns the raw parsed value when payload is not an object', () => { + expect(trimActivityData('"just a string"')).toBe('just a string'); + expect(trimActivityData('[1,2,3]')).toEqual([1, 2, 3]); + expect(trimActivityData('null')).toBeNull(); + }); + + it('drops previous_value when it deep-equals value', () => { + const files = [{ assetId: 1, name: 'a.pdf' }, { assetId: 2, name: 'b.pdf' }]; + const input = JSON.stringify({ + action_record_uuid: 'uuid-1', + column_id: 'files', + previous_value: files, + value: files, + }); + const out = trimActivityData(input) as Record; + expect(out).toEqual({ + action_record_uuid: 'uuid-1', + column_id: 'files', + value: files, + previous_value_omitted: 'equals_value', + }); + expect(out.previous_value).toBeUndefined(); + }); + + it('preserves both fields when previous_value differs from value', () => { + const input = JSON.stringify({ + previous_value: [1, 2, 3], + value: [1, 2, 3, 4], + }); + expect(trimActivityData(input)).toEqual({ previous_value: [1, 2, 3], value: [1, 2, 3, 4] }); + }); + + it('preserves action_record_uuid when trimming', () => { + const files = [{ assetId: 1 }]; + const input = JSON.stringify({ + action_record_uuid: 'must-survive', + previous_value: files, + value: files, + }); + const out = trimActivityData(input) as Record; + expect(out.action_record_uuid).toBe('must-survive'); + expect(out.previous_value_omitted).toBe('equals_value'); + }); +}); diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/get-board-activity/trim-activity-data.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/get-board-activity/trim-activity-data.ts new file mode 100644 index 00000000..17803327 --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/get-board-activity/trim-activity-data.ts @@ -0,0 +1,22 @@ +import equal from 'fast-deep-equal'; + +export function trimActivityData(dataJson: string): unknown { + let parsed: unknown; + try { + parsed = JSON.parse(dataJson); + } catch { + return dataJson; + } + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return parsed; + } + const obj = parsed as Record; + if (!('previous_value' in obj) || !('value' in obj)) { + return obj; + } + if (!equal(obj.previous_value, obj.value)) { + return obj; + } + const { previous_value: _drop, ...rest } = obj; + return { ...rest, previous_value_omitted: 'equals_value' }; +}