diff --git a/packages/agent-toolkit/package.json b/packages/agent-toolkit/package.json index d402346..bdd2c3a 100644 --- a/packages/agent-toolkit/package.json +++ b/packages/agent-toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@mondaydotcomorg/agent-toolkit", - "version": "2.20.3", + "version": "2.21.0", "description": "monday.com agent toolkit", "exports": { "./mcp": { diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/board-insights/board-insights-tool.test.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/board-insights/board-insights-tool.test.ts new file mode 100644 index 0000000..b73ac96 --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/board-insights/board-insights-tool.test.ts @@ -0,0 +1,1479 @@ +import { createMockApiClient } from '../test-utils/mock-api-client'; +import { BoardInsightsTool } from './board-insights-tool'; +import { handleFrom, handleFilters, handleSelectAndGroupByElements, handleOrderBy } from './board-insights-utils'; +import { + AggregateSelectFunctionName, + ItemsQueryOperator, + ItemsQueryRuleOperator, + AggregateFromElementType, + AggregateSelectElementType, + ItemsOrderByDirection, +} from 'src/monday-graphql/generated/graphql'; +import { DEFAULT_LIMIT } from './board-insights.consts'; + +describe('Board Insights Tool', () => { + describe('Utility Functions', () => { + describe('handleFrom', () => { + it('should create proper FROM clause for a board', () => { + const input = { boardId: 123456 }; + const result = handleFrom(input as any); + + expect(result).toEqual({ + id: '123456', + type: AggregateFromElementType.Table, + }); + }); + + it('should handle board ID as string', () => { + const input = { boardId: 987654 }; + const result = handleFrom(input as any); + + expect(result).toEqual({ + id: '987654', + type: AggregateFromElementType.Table, + }); + }); + }); + + describe('handleFilters', () => { + it('should return undefined when no filters provided', () => { + const input = { boardId: 123 }; + const result = handleFilters(input as any); + + expect(result).toBeUndefined(); + }); + + it('should transform single filter rule correctly', () => { + const input = { + boardId: 123, + filters: [ + { + columnId: 'status', + compareValue: 'Done', + operator: ItemsQueryRuleOperator.AnyOf, + }, + ], + filtersOperator: ItemsQueryOperator.And, + }; + + const result = handleFilters(input as any); + + expect(result).toEqual({ + rules: [ + { + column_id: 'status', + compare_value: 'Done', + operator: ItemsQueryRuleOperator.AnyOf, + compare_attribute: undefined, + }, + ], + operator: ItemsQueryOperator.And, + }); + }); + + it('should transform multiple filter rules correctly', () => { + const input = { + boardId: 123, + filters: [ + { + columnId: 'status', + compareValue: 'Done', + operator: ItemsQueryRuleOperator.AnyOf, + }, + { + columnId: 'person', + compareValue: [1234, 5678], + operator: ItemsQueryRuleOperator.AnyOf, + compareAttribute: 'id', + }, + ], + filtersOperator: ItemsQueryOperator.Or, + }; + + const result = handleFilters(input as any); + + expect(result).toEqual({ + rules: [ + { + column_id: 'status', + compare_value: 'Done', + operator: ItemsQueryRuleOperator.AnyOf, + compare_attribute: undefined, + }, + { + column_id: 'person', + compare_value: [1234, 5678], + operator: ItemsQueryRuleOperator.AnyOf, + compare_attribute: 'id', + }, + ], + operator: ItemsQueryOperator.Or, + }); + }); + + it('should include orderBy when provided', () => { + const input = { + boardId: 123, + orderBy: [ + { + columnId: 'status', + direction: ItemsOrderByDirection.Asc, + }, + ], + }; + + const result = handleFilters(input as any); + + expect(result).toEqual({ + order_by: [ + { + column_id: 'status', + direction: ItemsOrderByDirection.Asc, + }, + ], + }); + }); + + it('should include both filters and orderBy when both provided', () => { + const input = { + boardId: 123, + filters: [ + { + columnId: 'status', + compareValue: 'Done', + operator: ItemsQueryRuleOperator.AnyOf, + }, + ], + filtersOperator: ItemsQueryOperator.And, + orderBy: [ + { + columnId: 'created_at', + direction: ItemsOrderByDirection.Desc, + }, + ], + }; + + const result = handleFilters(input as any); + + expect(result).toEqual({ + rules: [ + { + column_id: 'status', + compare_value: 'Done', + operator: ItemsQueryRuleOperator.AnyOf, + compare_attribute: undefined, + }, + ], + operator: ItemsQueryOperator.And, + order_by: [ + { + column_id: 'created_at', + direction: ItemsOrderByDirection.Desc, + }, + ], + }); + }); + }); + + describe('handleOrderBy', () => { + it('should return undefined when no orderBy provided', () => { + const input = { boardId: 123 }; + const result = handleOrderBy(input as any); + + expect(result).toBeUndefined(); + }); + + it('should transform single orderBy correctly with ASC direction', () => { + const input = { + boardId: 123, + orderBy: [ + { + columnId: 'status', + direction: ItemsOrderByDirection.Asc, + }, + ], + }; + + const result = handleOrderBy(input as any); + + expect(result).toEqual([ + { + column_id: 'status', + direction: ItemsOrderByDirection.Asc, + }, + ]); + }); + + it('should transform single orderBy correctly with DESC direction', () => { + const input = { + boardId: 123, + orderBy: [ + { + columnId: 'created_at', + direction: ItemsOrderByDirection.Desc, + }, + ], + }; + + const result = handleOrderBy(input as any); + + expect(result).toEqual([ + { + column_id: 'created_at', + direction: ItemsOrderByDirection.Desc, + }, + ]); + }); + + it('should transform multiple orderBy correctly', () => { + const input = { + boardId: 123, + orderBy: [ + { + columnId: 'status', + direction: ItemsOrderByDirection.Asc, + }, + { + columnId: 'priority', + direction: ItemsOrderByDirection.Desc, + }, + { + columnId: 'created_at', + direction: ItemsOrderByDirection.Asc, + }, + ], + }; + + const result = handleOrderBy(input as any); + + expect(result).toEqual([ + { + column_id: 'status', + direction: ItemsOrderByDirection.Asc, + }, + { + column_id: 'priority', + direction: ItemsOrderByDirection.Desc, + }, + { + column_id: 'created_at', + direction: ItemsOrderByDirection.Asc, + }, + ]); + }); + }); + + describe('handleSelectAndGroupByElements', () => { + it('should handle simple column select without function', () => { + const input = { + boardId: 123, + aggregations: [{ columnId: 'status' }], + }; + + const result = handleSelectAndGroupByElements(input as any); + + expect(result.selectElements).toEqual([ + { + type: AggregateSelectElementType.Column, + column: { column_id: 'status' }, + as: 'status', + }, + ]); + + expect(result.groupByElements).toEqual([{ column_id: 'status' }]); + }); + + it('should handle aggregation function (COUNT)', () => { + const input = { + boardId: 123, + aggregations: [ + { + columnId: 'item_id', + function: AggregateSelectFunctionName.Count, + }, + ], + }; + + const result = handleSelectAndGroupByElements(input as any); + + expect(result.selectElements).toEqual([ + { + type: AggregateSelectElementType.Function, + function: { + function: AggregateSelectFunctionName.Count, + params: [ + { + type: AggregateSelectElementType.Column, + column: { column_id: 'item_id' }, + as: 'item_id', + }, + ], + }, + as: 'COUNT_item_id_0', + }, + ]); + + expect(result.groupByElements).toEqual([]); + }); + + it('should handle transformative function and add to group by', () => { + const input = { + boardId: 123, + aggregations: [ + { + columnId: 'status', + function: AggregateSelectFunctionName.Label, + }, + ], + limit: DEFAULT_LIMIT, + }; + + const result = handleSelectAndGroupByElements(input as any); + + expect(result.selectElements).toEqual([ + { + type: AggregateSelectElementType.Function, + function: { + function: AggregateSelectFunctionName.Label, + params: [ + { + type: AggregateSelectElementType.Column, + column: { column_id: 'status' }, + as: 'status', + }, + ], + }, + as: 'LABEL_status_0', + }, + ]); + + expect(result.groupByElements).toEqual([{ column_id: 'LABEL_status_0' }]); + }); + + it('should handle mixed aggregations with group by', () => { + const input = { + boardId: 123, + aggregations: [ + { columnId: 'status' }, + { + columnId: 'item_id', + function: AggregateSelectFunctionName.Count, + }, + ], + groupBy: ['status'], + }; + + const result = handleSelectAndGroupByElements(input as any); + + expect(result.selectElements).toHaveLength(2); + expect(result.selectElements[0]).toEqual({ + type: AggregateSelectElementType.Column, + column: { column_id: 'status' }, + as: 'status', + }); + expect(result.selectElements[1].type).toBe(AggregateSelectElementType.Function); + expect(result.groupByElements).toEqual([{ column_id: 'status' }]); + }); + + it('should add select elements for group by columns not in aggregations', () => { + const input = { + boardId: 123, + aggregations: [ + { + columnId: 'item_id', + function: AggregateSelectFunctionName.Count, + }, + ], + groupBy: ['status', 'priority'], + }; + + const result = handleSelectAndGroupByElements(input as any); + + expect(result.selectElements).toHaveLength(3); + expect(result.groupByElements).toEqual([{ column_id: 'status' }, { column_id: 'priority' }]); + + // Should have added column selects for status and priority + expect( + result.selectElements.some((el) => el.type === AggregateSelectElementType.Column && el.as === 'status'), + ).toBe(true); + expect( + result.selectElements.some((el) => el.type === AggregateSelectElementType.Column && el.as === 'priority'), + ).toBe(true); + }); + + it('should handle multiple aggregations of same column with different functions', () => { + const input = { + boardId: 123, + aggregations: [ + { + columnId: 'numbers', + function: AggregateSelectFunctionName.Sum, + }, + { + columnId: 'numbers', + function: AggregateSelectFunctionName.Average, + }, + { + columnId: 'numbers', + function: AggregateSelectFunctionName.Max, + }, + ], + }; + + const result = handleSelectAndGroupByElements(input as any); + + expect(result.selectElements).toHaveLength(3); + expect(result.selectElements[0].as).toBe('SUM_numbers_0'); + expect(result.selectElements[1].as).toBe('AVERAGE_numbers_0'); + expect(result.selectElements[2].as).toBe('MAX_numbers_0'); + }); + + it('should handle duplicate aggregations with incremented aliases', () => { + const input = { + boardId: 123, + aggregations: [ + { + columnId: 'numbers', + function: AggregateSelectFunctionName.Sum, + }, + { + columnId: 'numbers', + function: AggregateSelectFunctionName.Sum, + }, + ], + }; + + const result = handleSelectAndGroupByElements(input as any); + + expect(result.selectElements).toHaveLength(2); + expect(result.selectElements[0].as).toBe('SUM_numbers_0'); + expect(result.selectElements[1].as).toBe('SUM_numbers_1'); + }); + }); + }); + + describe('BoardInsightsTool Execution', () => { + let mocks: ReturnType; + + beforeEach(() => { + mocks = createMockApiClient(); + jest.clearAllMocks(); + }); + + it('should successfully get basic board insights', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { + alias: 'status', + value: { value_string: 'Done' }, + }, + { + alias: 'COUNT_item_id_0', + value: { result: 5 }, + }, + ], + }, + { + entries: [ + { + alias: 'status', + value: { value_string: 'Working on it' }, + }, + { + alias: 'COUNT_item_id_0', + value: { result: 3 }, + }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + const result = await tool.execute({ + boardId: 123456, + aggregations: [{ columnId: 'status' }, { columnId: 'item_id', function: AggregateSelectFunctionName.Count }], + groupBy: ['status'], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + expect(result.content).toContain('Board insights result (2 rows)'); + expect(result.content).toContain('"status": "Done"'); + expect(result.content).toContain('"COUNT_item_id_0": 5'); + expect(result.content).toContain('"status": "Working on it"'); + expect(result.content).toContain('"COUNT_item_id_0": 3'); + + expect(mocks.getMockRequest()).toHaveBeenCalledWith( + expect.stringContaining('query aggregateBoardInsights'), + expect.objectContaining({ + query: expect.objectContaining({ + from: { id: '123456', type: AggregateFromElementType.Table }, + }), + }), + ); + }); + + it('should handle insights with filters', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { + alias: 'COUNT_item_id_0', + value: { result: 10 }, + }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + const result = await tool.execute({ + boardId: 123456, + aggregations: [{ columnId: 'item_id', function: AggregateSelectFunctionName.Count }], + filters: [ + { + columnId: 'status', + compareValue: 'Done', + operator: ItemsQueryRuleOperator.AnyOf, + }, + ], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + expect(result.content).toContain('Board insights result (1 rows)'); + expect(result.content).toContain('"COUNT_item_id_0": 10'); + + const mockCall = mocks.getMockRequest().mock.calls[0]; + expect(mockCall[1].query.query).toEqual({ + rules: [ + { + column_id: 'status', + compare_value: 'Done', + operator: ItemsQueryRuleOperator.AnyOf, + compare_attribute: undefined, + }, + ], + operator: ItemsQueryOperator.And, + }); + }); + + it('should handle insights with limit', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { + alias: 'status', + value: { value_string: 'Done' }, + }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + await tool.execute({ + boardId: 123456, + aggregations: [{ columnId: 'status' }], + limit: 5, + filtersOperator: ItemsQueryOperator.And, + }); + + const mockCall = mocks.getMockRequest().mock.calls[0]; + expect(mockCall[1].query.limit).toBe(5); + }); + + it('should handle empty results', async () => { + const mockResponse = { + aggregate: { + results: [], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + const result = await tool.execute({ + boardId: 123456, + aggregations: [{ columnId: 'status' }], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + expect(result.content).toBe('No board insights found for the given query.'); + }); + + it('should handle null aggregate response', async () => { + const mockResponse = { + aggregate: null, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + const result = await tool.execute({ + boardId: 123456, + aggregations: [{ columnId: 'status' }], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + expect(result.content).toBe('No board insights found for the given query.'); + }); + + it('should handle different value types in results', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { + alias: 'string_col', + value: { value_string: 'text value' }, + }, + { + alias: 'int_col', + value: { value_int: 42 }, + }, + { + alias: 'float_col', + value: { value_float: 3.14 }, + }, + { + alias: 'bool_col', + value: { value_boolean: true }, + }, + { + alias: 'result_col', + value: { result: 100 }, + }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + const result = await tool.execute({ + boardId: 123456, + aggregations: [ + { columnId: 'string_col' }, + { columnId: 'int_col' }, + { columnId: 'float_col' }, + { columnId: 'bool_col' }, + { columnId: 'result_col', function: AggregateSelectFunctionName.Count }, + ], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + expect(result.content).toContain('"string_col": "text value"'); + expect(result.content).toContain('"int_col": 42'); + expect(result.content).toContain('"float_col": 3.14'); + expect(result.content).toContain('"bool_col": true'); + expect(result.content).toContain('"result_col": 100'); + }); + + it('should handle null values in results', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { + alias: 'status', + value: null, + }, + { + alias: 'count', + value: { result: 5 }, + }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + const result = await tool.execute({ + boardId: 123456, + aggregations: [{ columnId: 'status' }, { columnId: 'item_id', function: AggregateSelectFunctionName.Count }], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + expect(result.content).toContain('"status": null'); + expect(result.content).toContain('"count": 5'); + }); + + it('should handle entries with no alias', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { + alias: '', + value: { value_string: 'should be ignored' }, + }, + { + alias: 'status', + value: { value_string: 'Done' }, + }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + const result = await tool.execute({ + boardId: 123456, + aggregations: [{ columnId: 'status' }], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + const parsedResult = JSON.parse(result.content.split(':\n')[1]); + expect(parsedResult[0]).toEqual({ status: 'Done' }); + expect(parsedResult[0]).not.toHaveProperty(''); + }); + + it('should handle API errors', async () => { + mocks.setError('Board not found'); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + await expect( + tool.execute({ + boardId: 999999, + aggregations: [{ columnId: 'status' }], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }), + ).rejects.toThrow('Board not found'); + }); + + it('should handle GraphQL response errors', async () => { + const graphqlError = new Error('GraphQL Error'); + (graphqlError as any).response = { + errors: [{ message: 'Invalid column ID' }, { message: 'Access denied' }], + }; + mocks.setError(graphqlError); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + await expect( + tool.execute({ + boardId: 123456, + aggregations: [{ columnId: 'invalid_column' }], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }), + ).rejects.toThrow('GraphQL Error'); + }); + + it('should handle complex aggregation with multiple group by columns', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { alias: 'status', value: { value_string: 'Done' } }, + { alias: 'priority', value: { value_string: 'High' } }, + { alias: 'SUM_numbers_0', value: { result: 150 } }, + { alias: 'AVERAGE_numbers_0', value: { result: 30 } }, + ], + }, + { + entries: [ + { alias: 'status', value: { value_string: 'Done' } }, + { alias: 'priority', value: { value_string: 'Low' } }, + { alias: 'SUM_numbers_0', value: { result: 80 } }, + { alias: 'AVERAGE_numbers_0', value: { result: 20 } }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + const result = await tool.execute({ + boardId: 123456, + aggregations: [ + { columnId: 'status' }, + { columnId: 'priority' }, + { columnId: 'numbers', function: AggregateSelectFunctionName.Sum }, + { columnId: 'numbers', function: AggregateSelectFunctionName.Average }, + ], + groupBy: ['status', 'priority'], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + expect(result.content).toContain('Board insights result (2 rows)'); + expect(result.content).toContain('"status": "Done"'); + expect(result.content).toContain('"priority": "High"'); + expect(result.content).toContain('"SUM_numbers_0": 150'); + expect(result.content).toContain('"AVERAGE_numbers_0": 30'); + expect(result.content).toContain('"priority": "Low"'); + expect(result.content).toContain('"SUM_numbers_0": 80'); + expect(result.content).toContain('"AVERAGE_numbers_0": 20'); + }); + + it('should handle insights with single column orderBy', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { alias: 'status', value: { value_string: 'Done' } }, + { alias: 'COUNT_item_id_0', value: { result: 5 } }, + ], + }, + { + entries: [ + { alias: 'status', value: { value_string: 'Working on it' } }, + { alias: 'COUNT_item_id_0', value: { result: 3 } }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + await tool.execute({ + boardId: 123456, + aggregations: [{ columnId: 'status' }, { columnId: 'item_id', function: AggregateSelectFunctionName.Count }], + groupBy: ['status'], + orderBy: [ + { + columnId: 'status', + direction: ItemsOrderByDirection.Asc, + }, + ], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + const mockCall = mocks.getMockRequest().mock.calls[0]; + expect(mockCall[1].query.query).toEqual({ + order_by: [ + { + column_id: 'status', + direction: ItemsOrderByDirection.Asc, + }, + ], + }); + }); + + it('should handle insights with multiple column orderBy', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { alias: 'status', value: { value_string: 'Done' } }, + { alias: 'priority', value: { value_string: 'High' } }, + { alias: 'COUNT_item_id_0', value: { result: 2 } }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + await tool.execute({ + boardId: 123456, + aggregations: [ + { columnId: 'status' }, + { columnId: 'priority' }, + { columnId: 'item_id', function: AggregateSelectFunctionName.Count }, + ], + groupBy: ['status', 'priority'], + orderBy: [ + { + columnId: 'status', + direction: ItemsOrderByDirection.Asc, + }, + { + columnId: 'priority', + direction: ItemsOrderByDirection.Desc, + }, + ], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + const mockCall = mocks.getMockRequest().mock.calls[0]; + expect(mockCall[1].query.query).toEqual({ + order_by: [ + { + column_id: 'status', + direction: ItemsOrderByDirection.Asc, + }, + { + column_id: 'priority', + direction: ItemsOrderByDirection.Desc, + }, + ], + }); + }); + + it('should handle insights with filters and orderBy combined', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { alias: 'status', value: { value_string: 'Done' } }, + { alias: 'COUNT_item_id_0', value: { result: 8 } }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + await tool.execute({ + boardId: 123456, + aggregations: [{ columnId: 'status' }, { columnId: 'item_id', function: AggregateSelectFunctionName.Count }], + groupBy: ['status'], + filters: [ + { + columnId: 'priority', + compareValue: 'High', + operator: ItemsQueryRuleOperator.AnyOf, + }, + ], + filtersOperator: ItemsQueryOperator.And, + orderBy: [ + { + columnId: 'COUNT_item_id_0', + direction: ItemsOrderByDirection.Desc, + }, + ], + limit: DEFAULT_LIMIT, + }); + + const mockCall = mocks.getMockRequest().mock.calls[0]; + expect(mockCall[1].query.query).toEqual({ + rules: [ + { + column_id: 'priority', + compare_value: 'High', + operator: ItemsQueryRuleOperator.AnyOf, + compare_attribute: undefined, + }, + ], + operator: ItemsQueryOperator.And, + order_by: [ + { + column_id: 'COUNT_item_id_0', + direction: ItemsOrderByDirection.Desc, + }, + ], + }); + }); + + it('should handle orderBy with DESC direction', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { alias: 'created_at', value: { value_string: '2024-01-15' } }, + { alias: 'COUNT_item_id_0', value: { result: 10 } }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + await tool.execute({ + boardId: 123456, + aggregations: [ + { columnId: 'created_at' }, + { columnId: 'item_id', function: AggregateSelectFunctionName.Count }, + ], + groupBy: ['created_at'], + orderBy: [ + { + columnId: 'created_at', + direction: ItemsOrderByDirection.Desc, + }, + ], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + const mockCall = mocks.getMockRequest().mock.calls[0]; + expect(mockCall[1].query.query.order_by[0].direction).toBe(ItemsOrderByDirection.Desc); + }); + + it('should count items using COUNT_ITEMS function', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { + alias: 'COUNT_ITEMS_item_id_0', + value: { result: 42 }, + }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + const result = await tool.execute({ + boardId: 123456, + aggregations: [ + { + columnId: 'item_id', + function: AggregateSelectFunctionName.CountItems, + }, + ], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + expect(result.content).toContain('Board insights result (1 rows)'); + expect(result.content).toContain('"COUNT_ITEMS_item_id_0": 42'); + + expect(mocks.getMockRequest()).toHaveBeenCalledWith( + expect.stringContaining('query aggregateBoardInsights'), + expect.objectContaining({ + query: expect.objectContaining({ + from: { id: '123456', type: AggregateFromElementType.Table }, + select: expect.arrayContaining([ + expect.objectContaining({ + type: AggregateSelectElementType.Function, + function: expect.objectContaining({ + function: AggregateSelectFunctionName.CountItems, + }), + }), + ]), + }), + }), + ); + }); + + it('should count items with filters applied', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { + alias: 'COUNT_ITEMS_item_id_0', + value: { result: 15 }, + }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + const result = await tool.execute({ + boardId: 123456, + aggregations: [ + { + columnId: 'item_id', + function: AggregateSelectFunctionName.CountItems, + }, + ], + filters: [ + { + columnId: 'status', + compareValue: 'Done', + operator: ItemsQueryRuleOperator.AnyOf, + }, + ], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + expect(result.content).toContain('Board insights result (1 rows)'); + expect(result.content).toContain('"COUNT_ITEMS_item_id_0": 15'); + + const mockCall = mocks.getMockRequest().mock.calls[0]; + expect(mockCall[1].query.query).toEqual({ + rules: [ + { + column_id: 'status', + compare_value: 'Done', + operator: ItemsQueryRuleOperator.AnyOf, + compare_attribute: undefined, + }, + ], + operator: ItemsQueryOperator.And, + }); + }); + + it('should have correct metadata', () => { + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + expect(tool.name).toBe('board_insights'); + expect(tool.type).toBe('read'); + expect(tool.getDescription()).toBe( + "This tool allows you to calculate insights about board's data by filtering, grouping and aggregating columns. For example, you can get the total number of items in a board, the number of items in each status, the number of items in each column, etc. " + + "Use this tool when you need to get a summary of the board's data, for example, you want to know the total number of items in a board, the number of items in each status, the number of items in each column, etc." + + "[REQUIRED PRECONDITION]: Before using this tool, if you are not familiar with the board's structure (column IDs, column types, status labels, etc.), first use get_board_info to understand the board metadata. This is essential for constructing proper filters and knowing which columns are available.", + ); + expect(tool.annotations.title).toBe('Get Board Insights'); + expect(tool.annotations.readOnlyHint).toBe(true); + expect(tool.annotations.destructiveHint).toBe(false); + expect(tool.annotations.idempotentHint).toBe(true); + }); + }); + + describe('Stringified Fields (Microsoft Copilot compatibility)', () => { + let mocks: ReturnType; + + beforeEach(() => { + mocks = createMockApiClient(); + jest.clearAllMocks(); + }); + + it('should handle aggregationsStringified field', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { + alias: 'status', + value: { value_string: 'Done' }, + }, + { + alias: 'COUNT_item_id_0', + value: { result: 5 }, + }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + const aggregations = [ + { columnId: 'status' }, + { columnId: 'item_id', function: AggregateSelectFunctionName.Count }, + ]; + + const result = await tool.execute({ + boardId: 123456, + aggregationsStringified: JSON.stringify(aggregations), + groupBy: ['status'], + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + expect(result.content).toContain('Board insights result (1 rows)'); + expect(result.content).toContain('"status": "Done"'); + expect(result.content).toContain('"COUNT_item_id_0": 5'); + + expect(mocks.getMockRequest()).toHaveBeenCalledWith( + expect.stringContaining('query aggregateBoardInsights'), + expect.objectContaining({ + query: expect.objectContaining({ + from: { id: '123456', type: AggregateFromElementType.Table }, + }), + }), + ); + }); + + it('should handle filtersStringified field', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { + alias: 'COUNT_item_id_0', + value: { result: 10 }, + }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + const filters = [ + { + columnId: 'status', + compareValue: 'Done', + operator: ItemsQueryRuleOperator.AnyOf, + }, + ]; + + const result = await tool.execute({ + boardId: 123456, + aggregations: [{ columnId: 'item_id', function: AggregateSelectFunctionName.Count }], + filtersStringified: JSON.stringify(filters), + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + expect(result.content).toContain('Board insights result (1 rows)'); + expect(result.content).toContain('"COUNT_item_id_0": 10'); + + const mockCall = mocks.getMockRequest().mock.calls[0]; + expect(mockCall[1].query.query).toEqual({ + rules: [ + { + column_id: 'status', + compare_value: 'Done', + operator: ItemsQueryRuleOperator.AnyOf, + compare_attribute: undefined, + }, + ], + operator: ItemsQueryOperator.And, + }); + }); + + it('should handle orderByStringified field', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { alias: 'status', value: { value_string: 'Done' } }, + { alias: 'COUNT_item_id_0', value: { result: 5 } }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + const orderBy = [ + { + columnId: 'status', + direction: ItemsOrderByDirection.Asc, + }, + ]; + + const result = await tool.execute({ + boardId: 123456, + aggregations: [{ columnId: 'status' }, { columnId: 'item_id', function: AggregateSelectFunctionName.Count }], + groupBy: ['status'], + orderByStringified: JSON.stringify(orderBy), + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + expect(result.content).toContain('Board insights result (1 rows)'); + + const mockCall = mocks.getMockRequest().mock.calls[0]; + expect(mockCall[1].query.query).toEqual({ + order_by: [ + { + column_id: 'status', + direction: ItemsOrderByDirection.Asc, + }, + ], + }); + }); + + it('should handle all stringified fields together', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { alias: 'status', value: { value_string: 'Done' } }, + { alias: 'COUNT_item_id_0', value: { result: 8 } }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + const aggregations = [ + { columnId: 'status' }, + { columnId: 'item_id', function: AggregateSelectFunctionName.Count }, + ]; + + const filters = [ + { + columnId: 'priority', + compareValue: 'High', + operator: ItemsQueryRuleOperator.AnyOf, + }, + ]; + + const orderBy = [ + { + columnId: 'COUNT_item_id_0', + direction: ItemsOrderByDirection.Desc, + }, + ]; + + const result = await tool.execute({ + boardId: 123456, + aggregationsStringified: JSON.stringify(aggregations), + groupBy: ['status'], + filtersStringified: JSON.stringify(filters), + filtersOperator: ItemsQueryOperator.And, + orderByStringified: JSON.stringify(orderBy), + limit: DEFAULT_LIMIT, + }); + + expect(result.content).toContain('Board insights result (1 rows)'); + expect(result.content).toContain('"status": "Done"'); + expect(result.content).toContain('"COUNT_item_id_0": 8'); + + const mockCall = mocks.getMockRequest().mock.calls[0]; + expect(mockCall[1].query.query).toEqual({ + rules: [ + { + column_id: 'priority', + compare_value: 'High', + operator: ItemsQueryRuleOperator.AnyOf, + compare_attribute: undefined, + }, + ], + operator: ItemsQueryOperator.And, + order_by: [ + { + column_id: 'COUNT_item_id_0', + direction: ItemsOrderByDirection.Desc, + }, + ], + }); + }); + + it('should return error when neither aggregations nor aggregationsStringified is provided', async () => { + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + const result = await tool.execute({ + boardId: 123456, + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + expect(result.content).toBe( + 'Input must contain either the "aggregations" field or the "aggregationsStringified" field.', + ); + }); + + it('should prefer non-stringified field over stringified when both provided', async () => { + const mockResponse = { + aggregate: { + results: [ + { + entries: [ + { + alias: 'priority', + value: { value_string: 'High' }, + }, + ], + }, + ], + }, + }; + + mocks.setResponse(mockResponse); + + const tool = new BoardInsightsTool(mocks.mockApiClient, 'fake_token'); + + // Non-stringified has priority + const correctAggregations = [{ columnId: 'priority' }]; + + // Stringified should be ignored + const wrongAggregations = [{ columnId: 'status' }]; + + const result = await tool.execute({ + boardId: 123456, + aggregations: correctAggregations, + aggregationsStringified: JSON.stringify(wrongAggregations), + filtersOperator: ItemsQueryOperator.And, + limit: DEFAULT_LIMIT, + }); + + expect(result.content).toContain('Board insights result (1 rows)'); + expect(result.content).toContain('"priority": "High"'); + + // Should use priority, not status + const mockCall = mocks.getMockRequest().mock.calls[0]; + expect(mockCall[1].query.select).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + column: { column_id: 'priority' }, + }), + ]), + ); + }); + }); +}); diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/board-insights/board-insights-tool.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/board-insights/board-insights-tool.ts new file mode 100644 index 0000000..c3443bc --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/board-insights/board-insights-tool.ts @@ -0,0 +1,173 @@ +import { z } from 'zod'; +import { ToolInputType, ToolOutputType, ToolType } from '../../../tool'; +import { BaseMondayApiTool, createMondayApiAnnotations } from '../base-monday-api-tool'; +import { boardInsights } from './board-insights.graphql'; +import { + ItemsQueryOperator, + ItemsQueryRuleOperator, + AggregateBoardInsightsQueryVariables, + AggregateBoardInsightsQuery, + ItemsOrderByDirection, +} from 'src/monday-graphql/generated/graphql'; +import { handleFilters, handleFrom, handleSelectAndGroupByElements } from './board-insights-utils'; +import { BoardInsightsAggregationFunction, DEFAULT_LIMIT, MAX_LIMIT } from './board-insights.consts'; +import { fallbackToStringifiedVersionIfNull } from '../../shared/microsoft-copilot-utils'; + +export const boardInsightsToolSchema = { + boardId: z.number().describe('The id of the board to get insights for'), + aggregationsStringified: z + .string() + .optional() + .describe( + '**ONLY FOR MICROSOFT COPILOT**: The aggregations to get. Send this as a stringified JSON array of "aggregations" field. Read "aggregations" field description for details how to use it.', + ), + aggregations: z + .array( + z.object({ + function: z + .enum(BoardInsightsAggregationFunction) + .describe('The function of the aggregation. For simple column value leave undefined') + .optional(), + columnId: z.string().describe('The id of the column to aggregate'), + }), + ) + .describe( + 'The aggregations to get. Transformative functions and plain columns (no function) must be in group by. [REQUIRED PRECONDITION]: Either send this field or the stringified version of it.', + ) + .optional(), + groupBy: z + .array(z.string()) + .describe( + 'The columns to group by. All columns in the group by must be in the aggregations as well without a function.', + ) + .optional(), + limit: z.number().describe('The limit of the results').max(MAX_LIMIT).optional().default(DEFAULT_LIMIT), + filtersStringified: z + .string() + .optional() + .describe( + '**ONLY FOR MICROSOFT COPILOT**: The filters to apply on the items. Send this as a stringified JSON array of "filters" field. Read "filters" field description for details how to use it.', + ), + filters: z + .array( + z.object({ + columnId: z.string().describe('The id of the column to filter by'), + compareAttribute: z.string().optional().describe('The attribute to compare the value to'), + compareValue: z + .any() + .describe( + 'The value to compare the attribute to. This can be a string or index value depending on the column type.', + ), + operator: z + .nativeEnum(ItemsQueryRuleOperator) + .optional() + .default(ItemsQueryRuleOperator.AnyOf) + .describe('The operator to use for the filter'), + }), + ) + .optional() + .describe( + 'The configuration of filters to apply on the items. Before sending the filters, use get_board_info tool to check "Filtering Guidelines" section for filtering by the column.', + ), + filtersOperator: z + .nativeEnum(ItemsQueryOperator) + .optional() + .default(ItemsQueryOperator.And) + .describe('The logical operator to use for the filters'), + + orderByStringified: z + .string() + .optional() + .describe( + '**ONLY FOR MICROSOFT COPILOT**: The order by to apply on the items. Send this as a stringified JSON array of "orderBy" field. Read "orderBy" field description for details how to use it.', + ), + orderBy: z + .array( + z.object({ + columnId: z.string().describe('The id of the column to order by'), + direction: z + .nativeEnum(ItemsOrderByDirection) + .optional() + .default(ItemsOrderByDirection.Asc) + .describe('The direction to order by'), + }), + ) + .optional() + .describe('The columns to order by, will control the order of the items in the response'), +}; + +export class BoardInsightsTool extends BaseMondayApiTool { + name = 'board_insights'; + type = ToolType.READ; + annotations = createMondayApiAnnotations({ + title: 'Get Board Insights', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + }); + + getDescription(): string { + return ( + "This tool allows you to calculate insights about board's data by filtering, grouping and aggregating columns. For example, you can get the total number of items in a board, the number of items in each status, the number of items in each column, etc. " + + "Use this tool when you need to get a summary of the board's data, for example, you want to know the total number of items in a board, the number of items in each status, the number of items in each column, etc." + + "[REQUIRED PRECONDITION]: Before using this tool, if you are not familiar with the board's structure (column IDs, column types, status labels, etc.), first use get_board_info to understand the board metadata. This is essential for constructing proper filters and knowing which columns are available." + ); + } + + getInputSchema(): typeof boardInsightsToolSchema { + return boardInsightsToolSchema; + } + + protected async executeInternal( + input: ToolInputType, + ): Promise> { + if (!input.aggregations && !input.aggregationsStringified) { + return { content: 'Input must contain either the "aggregations" field or the "aggregationsStringified" field.' }; + } + + fallbackToStringifiedVersionIfNull(input, 'aggregations', boardInsightsToolSchema.aggregations); + fallbackToStringifiedVersionIfNull(input, 'filters', boardInsightsToolSchema.filters); + fallbackToStringifiedVersionIfNull(input, 'orderBy', boardInsightsToolSchema.orderBy); + + const { selectElements, groupByElements } = handleSelectAndGroupByElements(input); + const filters = handleFilters(input); + const from = handleFrom(input); + + const variables: AggregateBoardInsightsQueryVariables = { + query: { + from, + query: filters, + select: selectElements, + group_by: groupByElements, + limit: input.limit, + }, + }; + + const res = await this.mondayApi.request(boardInsights, variables); + + const rows = (res.aggregate?.results ?? []).map((resultSet) => { + const row: Record = {}; + (resultSet.entries ?? []).forEach((entry) => { + const alias = entry.alias ?? ''; + if (!alias) return; + const value = entry.value as any; + if (!value) { + row[alias] = null; + return; + } + const v = + value.result ?? value.value_string ?? value.value_int ?? value.value_float ?? value.value_boolean ?? null; + row[alias] = v; + }); + return row; + }); + + if (!rows.length) { + return { content: 'No board insights found for the given query.' }; + } + + return { + content: `Board insights result (${rows.length} rows):\n${JSON.stringify(rows, null, 2)}`, + }; + } +} diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/board-insights/board-insights-utils.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/board-insights/board-insights-utils.ts new file mode 100644 index 0000000..b53fe67 --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/board-insights/board-insights-utils.ts @@ -0,0 +1,143 @@ +import { ToolInputType } from 'src/core/tool'; +import { boardInsightsToolSchema } from './board-insights-tool'; +import { + AggregateFromElementType, + AggregateFromTableInput, + AggregateGroupByElementInput, + AggregateSelectColumnInput, + AggregateSelectElementInput, + AggregateSelectElementType, + AggregateSelectFunctionInput, + AggregateSelectFunctionName, + ItemsQuery, + ItemsQueryOrderBy, +} from 'src/monday-graphql/generated/graphql'; +import { transformativeFunctions } from './board-insights.consts'; + +export function handleFrom(input: ToolInputType): AggregateFromTableInput { + return { + id: input.boardId.toString(), + type: AggregateFromElementType.Table, + }; +} + +export function handleFilters(input: ToolInputType): ItemsQuery | undefined { + if (!input.filters && !input.orderBy) { + return undefined; + } + const filters: ItemsQuery = {}; + if (input.filters) { + filters.rules = input.filters.map((rule) => ({ + column_id: rule.columnId, + compare_value: rule.compareValue, + operator: rule.operator, + compare_attribute: rule.compareAttribute, + })); + filters.operator = input.filtersOperator; + } + if (input.orderBy) { + filters.order_by = handleOrderBy(input); + } + return filters; +} + +function handleSelectColumnElement(columnId: string): AggregateSelectColumnInput { + return { + column_id: columnId, + }; +} + +function handleSelectFunctionElement( + functionName: AggregateSelectFunctionName, + columnId: string, +): AggregateSelectFunctionInput { + // special case: count items has no params + return { + function: functionName, + params: + functionName === AggregateSelectFunctionName.CountItems + ? [] + : [ + { + type: AggregateSelectElementType.Column, + column: handleSelectColumnElement(columnId), + as: columnId, + }, + ], + }; +} + +export function handleOrderBy(input: ToolInputType): ItemsQueryOrderBy[] | undefined { + return input.orderBy?.map((orderBy) => ({ + column_id: orderBy.columnId, + direction: orderBy.direction, + })); +} + +export function handleSelectAndGroupByElements(input: ToolInputType): { + selectElements: AggregateSelectElementInput[]; + groupByElements: AggregateGroupByElementInput[]; +} { + const aliasKeyMap: Record = {}; + + const groupByElements: AggregateGroupByElementInput[] = + input.groupBy?.map((columnId) => ({ + column_id: columnId, + })) || []; + + const selectElements = input.aggregations!.map((aggregation) => { + // handle a function + if (aggregation.function) { + // create a unique alias for the select element + const elementKey = `${aggregation.function}_${aggregation.columnId}`; + const aliasKeyIndex = aliasKeyMap[elementKey] || 0; + aliasKeyMap[elementKey] = aliasKeyIndex + 1; + const alias = `${elementKey}_${aliasKeyIndex}`; + + if (transformativeFunctions.has(aggregation.function)) { + // transformative functions must be in group by + if (!groupByElements.some((groupByElement) => groupByElement.column_id === alias)) { + // if not in group by, add to group by + groupByElements.push({ + column_id: alias, + }); + } + } + + const selectElement: AggregateSelectElementInput = { + type: AggregateSelectElementType.Function, + function: handleSelectFunctionElement(aggregation.function, aggregation.columnId), + as: alias, + }; + return selectElement; + } + + // handle a column + const selectElement: AggregateSelectElementInput = { + type: AggregateSelectElementType.Column, + column: handleSelectColumnElement(aggregation.columnId), + as: aggregation.columnId, + }; + // plain columns must be in group by. add if not already in group by + if (!groupByElements.some((groupByElement) => groupByElement.column_id === aggregation.columnId)) { + groupByElements.push({ + column_id: aggregation.columnId, + }); + } + return selectElement; + }); + + groupByElements.forEach((groupByElement) => { + // check if theres a group by element with no matching select + if (!selectElements.some((selectElement) => selectElement.as === groupByElement.column_id)) { + // if no matching select, add a column select element + selectElements.push({ + type: AggregateSelectElementType.Column, + column: handleSelectColumnElement(groupByElement.column_id), + as: groupByElement.column_id, + }); + } + }); + + return { selectElements, groupByElements }; +} diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/board-insights/board-insights.consts.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/board-insights/board-insights.consts.ts new file mode 100644 index 0000000..d9d5247 --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/board-insights/board-insights.consts.ts @@ -0,0 +1,58 @@ +import { AggregateSelectFunctionName } from 'src/monday-graphql/generated/graphql'; + +export const DEFAULT_LIMIT = 1000; +export const MAX_LIMIT = 1000; + +// Functions to exclude from BoardInsightsAggregationFunction +const excludedFunctions = new Set([ + AggregateSelectFunctionName.Case, + AggregateSelectFunctionName.Between, + AggregateSelectFunctionName.Left, + AggregateSelectFunctionName.Raw, + AggregateSelectFunctionName.None, + AggregateSelectFunctionName.CountKeys, +]); + +// Programmatically create array of allowed aggregation functions +export const BoardInsightsAggregationFunction = Object.values(AggregateSelectFunctionName).filter( + (fn) => !excludedFunctions.has(fn), +) as [AggregateSelectFunctionName, ...AggregateSelectFunctionName[]]; + +export const transformativeFunctions = new Set([ + AggregateSelectFunctionName.Left, + AggregateSelectFunctionName.Trim, + AggregateSelectFunctionName.Upper, + AggregateSelectFunctionName.Lower, + AggregateSelectFunctionName.DateTruncDay, + AggregateSelectFunctionName.DateTruncWeek, + AggregateSelectFunctionName.DateTruncMonth, + AggregateSelectFunctionName.DateTruncQuarter, + AggregateSelectFunctionName.DateTruncYear, + AggregateSelectFunctionName.Color, + AggregateSelectFunctionName.Label, + AggregateSelectFunctionName.EndDate, + AggregateSelectFunctionName.StartDate, + AggregateSelectFunctionName.Hour, + AggregateSelectFunctionName.PhoneCountryShortName, + AggregateSelectFunctionName.Person, + AggregateSelectFunctionName.Upper, + AggregateSelectFunctionName.Lower, + AggregateSelectFunctionName.Order, + AggregateSelectFunctionName.Length, + AggregateSelectFunctionName.Flatten, + AggregateSelectFunctionName.IsDone, +]); + +export const aggregativeFunctions = new Set([ + AggregateSelectFunctionName.Count, + AggregateSelectFunctionName.CountDistinct, + AggregateSelectFunctionName.CountSubitems, + AggregateSelectFunctionName.CountItems, + AggregateSelectFunctionName.First, + AggregateSelectFunctionName.Sum, + AggregateSelectFunctionName.Average, + AggregateSelectFunctionName.Median, + AggregateSelectFunctionName.Min, + AggregateSelectFunctionName.Max, + AggregateSelectFunctionName.MinMax, +]); diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/board-insights/board-insights.graphql.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/board-insights/board-insights.graphql.ts new file mode 100644 index 0000000..c21b057 --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/board-insights/board-insights.graphql.ts @@ -0,0 +1,24 @@ +import { gql } from 'graphql-request'; + +export const boardInsights = gql` + query aggregateBoardInsights($query: AggregateQueryInput!) { + aggregate(query: $query) { + results { + entries { + alias + value { + ... on AggregateBasicAggregationResult { + result + } + ... on AggregateGroupByResult { + value_string + value_int + value_float + value_boolean + } + } + } + } + } + } +`; diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/index.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/index.ts index b828b42..3f59325 100644 --- a/packages/agent-toolkit/src/core/tools/platform-api-tools/index.ts +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/index.ts @@ -36,6 +36,7 @@ import { UpdateFolderTool } from './update-folder-tool/update-folder-tool'; import { CreateWorkspaceTool } from './create-workspace-tool/create-workspace-tool'; import { CreateFolderTool } from './create-folder-tool/create-folder-tool'; import { MoveObjectTool } from './move-object-tool/move-object-tool'; +import { BoardInsightsTool } from './board-insights/board-insights-tool'; export const allGraphqlApiTools: BaseMondayApiToolConstructor[] = [ DeleteItemTool, @@ -75,7 +76,8 @@ export const allGraphqlApiTools: BaseMondayApiToolConstructor[] = [ // Dashboard Tools CreateDashboardTool, AllWidgetsSchemaTool, - CreateWidgetTool + CreateWidgetTool, + BoardInsightsTool, ]; export * from './all-monday-api-tool'; @@ -113,6 +115,7 @@ export * from './update-folder-tool/update-folder-tool'; export * from './create-workspace-tool/create-workspace-tool'; export * from './create-folder-tool/create-folder-tool'; export * from './move-object-tool/move-object-tool'; +export * from './board-insights/board-insights-tool'; // Dashboard Tools export * from './dashboard-tools'; // Monday Dev Tools diff --git a/packages/agent-toolkit/src/monday-graphql/generated/graphql.ts b/packages/agent-toolkit/src/monday-graphql/generated/graphql.ts index 61d9bf0..34cf15d 100644 --- a/packages/agent-toolkit/src/monday-graphql/generated/graphql.ts +++ b/packages/agent-toolkit/src/monday-graphql/generated/graphql.ts @@ -8918,6 +8918,44 @@ export type GetSprintsBoardItemsWithColumnsQueryVariables = Exact<{ export type GetSprintsBoardItemsWithColumnsQuery = { __typename?: 'Query', boards?: Array<{ __typename?: 'Board', items_page: { __typename?: 'ItemsResponse', items: Array<{ __typename?: 'Item', id: string, name: string, column_values: Array<{ __typename: 'BatteryValue', id: string, type: ColumnType } | { __typename: 'BoardRelationValue', id: string, type: ColumnType } | { __typename: 'ButtonValue', id: string, type: ColumnType } | { __typename: 'CheckboxValue', checked?: boolean | null, id: string, type: ColumnType } | { __typename: 'ColorPickerValue', id: string, type: ColumnType } | { __typename: 'CountryValue', id: string, type: ColumnType } | { __typename: 'CreationLogValue', id: string, type: ColumnType } | { __typename: 'DateValue', date?: string | null, id: string, type: ColumnType } | { __typename: 'DependencyValue', id: string, type: ColumnType } | { __typename: 'DirectDocValue', id: string, type: ColumnType } | { __typename: 'DocValue', id: string, type: ColumnType, file?: { __typename?: 'FileDocValue', doc: { __typename?: 'Document', object_id: string } } | null } | { __typename: 'DropdownValue', id: string, type: ColumnType } | { __typename: 'EmailValue', id: string, type: ColumnType } | { __typename: 'FileValue', id: string, type: ColumnType } | { __typename: 'FormulaValue', id: string, type: ColumnType } | { __typename: 'GroupValue', id: string, type: ColumnType } | { __typename: 'HourValue', id: string, type: ColumnType } | { __typename: 'IntegrationValue', id: string, type: ColumnType } | { __typename: 'ItemIdValue', id: string, type: ColumnType } | { __typename: 'LastUpdatedValue', id: string, type: ColumnType } | { __typename: 'LinkValue', id: string, type: ColumnType } | { __typename: 'LocationValue', id: string, type: ColumnType } | { __typename: 'LongTextValue', id: string, type: ColumnType } | { __typename: 'MirrorValue', id: string, type: ColumnType } | { __typename: 'NumbersValue', id: string, type: ColumnType } | { __typename: 'PeopleValue', id: string, type: ColumnType } | { __typename: 'PersonValue', id: string, type: ColumnType } | { __typename: 'PhoneValue', id: string, type: ColumnType } | { __typename: 'ProgressValue', id: string, type: ColumnType } | { __typename: 'RatingValue', id: string, type: ColumnType } | { __typename: 'StatusValue', id: string, type: ColumnType } | { __typename: 'SubtasksValue', id: string, type: ColumnType } | { __typename: 'TagsValue', id: string, type: ColumnType } | { __typename: 'TeamValue', id: string, type: ColumnType } | { __typename: 'TextValue', value?: any | null, id: string, type: ColumnType } | { __typename: 'TimeTrackingValue', id: string, type: ColumnType } | { __typename: 'TimelineValue', from?: any | null, to?: any | null, id: string, type: ColumnType } | { __typename: 'UnsupportedValue', id: string, type: ColumnType } | { __typename: 'VoteValue', id: string, type: ColumnType } | { __typename: 'WeekValue', id: string, type: ColumnType } | { __typename: 'WorldClockValue', id: string, type: ColumnType }> }> } } | null> | null }; +export type AggregateBoardInsightsQueryVariables = Exact<{ + query: AggregateQueryInput; +}>; + + +export type AggregateBoardInsightsQuery = { __typename?: 'Query', aggregate?: { __typename?: 'AggregateQueryResult', results?: Array<{ __typename?: 'AggregateResultSet', entries?: Array<{ __typename?: 'AggregateResultEntry', alias?: string | null, value?: { __typename?: 'AggregateBasicAggregationResult', result?: number | null } | { __typename?: 'AggregateGroupByResult', value_string?: string | null, value_int?: number | null, value_float?: number | null, value_boolean?: boolean | null } | null }> | null }> | null } | null }; + +export type GetItemBoardQueryVariables = Exact<{ + itemId: Scalars['ID']['input']; +}>; + + +export type GetItemBoardQuery = { __typename?: 'Query', items?: Array<{ __typename?: 'Item', id: string, board?: { __typename?: 'Board', id: string, columns?: Array<{ __typename?: 'Column', id: string, type: ColumnType } | null> | null } | null } | null> | null }; + +export type CreateDocMutationVariables = Exact<{ + location: CreateDocInput; +}>; + + +export type CreateDocMutation = { __typename?: 'Mutation', create_doc?: { __typename?: 'Document', id: string, url?: string | null, name: string } | null }; + +export type AddContentToDocFromMarkdownMutationVariables = Exact<{ + docId: Scalars['ID']['input']; + markdown: Scalars['String']['input']; + afterBlockId?: InputMaybe; +}>; + + +export type AddContentToDocFromMarkdownMutation = { __typename?: 'Mutation', add_content_to_doc_from_markdown?: { __typename?: 'DocBlocksFromMarkdownResult', success: boolean, block_ids?: Array | null, error?: string | null } | null }; + +export type UpdateDocNameMutationVariables = Exact<{ + docId: Scalars['ID']['input']; + name: Scalars['String']['input']; +}>; + + +export type UpdateDocNameMutation = { __typename?: 'Mutation', update_doc_name?: any | null }; + export type CreateFolderMutationVariables = Exact<{ workspaceId: Scalars['ID']['input']; name: Scalars['String']['input']; @@ -9467,37 +9505,6 @@ export type FetchCustomActivityQueryVariables = Exact<{ [key: string]: never; }> export type FetchCustomActivityQuery = { __typename?: 'Query', custom_activity?: Array<{ __typename?: 'CustomActivity', color?: CustomActivityColor | null, icon_id?: CustomActivityIcon | null, id?: string | null, name?: string | null, type?: string | null }> | null }; -export type GetItemBoardQueryVariables = Exact<{ - itemId: Scalars['ID']['input']; -}>; - - -export type GetItemBoardQuery = { __typename?: 'Query', items?: Array<{ __typename?: 'Item', id: string, board?: { __typename?: 'Board', id: string, columns?: Array<{ __typename?: 'Column', id: string, type: ColumnType } | null> | null } | null } | null> | null }; - -export type CreateDocMutationVariables = Exact<{ - location: CreateDocInput; -}>; - - -export type CreateDocMutation = { __typename?: 'Mutation', create_doc?: { __typename?: 'Document', id: string, url?: string | null, name: string } | null }; - -export type AddContentToDocFromMarkdownMutationVariables = Exact<{ - docId: Scalars['ID']['input']; - markdown: Scalars['String']['input']; - afterBlockId?: InputMaybe; -}>; - - -export type AddContentToDocFromMarkdownMutation = { __typename?: 'Mutation', add_content_to_doc_from_markdown?: { __typename?: 'DocBlocksFromMarkdownResult', success: boolean, block_ids?: Array | null, error?: string | null } | null }; - -export type UpdateDocNameMutationVariables = Exact<{ - docId: Scalars['ID']['input']; - name: Scalars['String']['input']; -}>; - - -export type UpdateDocNameMutation = { __typename?: 'Mutation', update_doc_name?: any | null }; - export type ReadDocsQueryVariables = Exact<{ ids?: InputMaybe | Scalars['ID']['input']>; object_ids?: InputMaybe | Scalars['ID']['input']>;