diff --git a/workspace-server/src/__tests__/services/PeopleService.test.ts b/workspace-server/src/__tests__/services/PeopleService.test.ts index 6c7d727..f1bc8f6 100644 --- a/workspace-server/src/__tests__/services/PeopleService.test.ts +++ b/workspace-server/src/__tests__/services/PeopleService.test.ts @@ -146,4 +146,130 @@ describe('PeopleService', () => { expect(JSON.parse(result.content[0].text)).toEqual({ error: 'API Error' }); }); }); -}); + + describe('getUserRelations', () => { + it('should return all relations when no relationType is specified', async () => { + const mockRelations = { + data: { + relations: [ + { person: 'John Doe', type: 'manager' }, + { person: 'Jane Doe', type: 'spouse' }, + { person: 'Bob Smith', type: 'assistant' }, + ], + }, + }; + mockPeopleAPI.people.get.mockResolvedValue(mockRelations); + + const result = await peopleService.getUserRelations({}); + + expect(mockPeopleAPI.people.get).toHaveBeenCalledWith({ + resourceName: 'people/me', + personFields: 'relations', + }); + expect(JSON.parse(result.content[0].text)).toEqual({ + resourceName: 'people/me', + relations: mockRelations.data.relations, + }); + }); + + it('should filter relations by relationType when specified', async () => { + const mockRelations = { + data: { + relations: [ + { person: 'John Doe', type: 'manager' }, + { person: 'Jane Doe', type: 'spouse' }, + { person: 'Bob Smith', type: 'assistant' }, + ], + }, + }; + mockPeopleAPI.people.get.mockResolvedValue(mockRelations); + + const result = await peopleService.getUserRelations({ relationType: 'manager' }); + + expect(JSON.parse(result.content[0].text)).toEqual({ + resourceName: 'people/me', + relationType: 'manager', + relations: [{ person: 'John Doe', type: 'manager' }], + }); + }); + + it('should filter relations case-insensitively', async () => { + const mockRelations = { + data: { + relations: [ + { person: 'John Doe', type: 'Manager' }, + ], + }, + }; + mockPeopleAPI.people.get.mockResolvedValue(mockRelations); + + const result = await peopleService.getUserRelations({ relationType: 'MANAGER' }); + + expect(JSON.parse(result.content[0].text)).toEqual({ + resourceName: 'people/me', + relationType: 'MANAGER', + relations: [{ person: 'John Doe', type: 'Manager' }], + }); + }); + + it('should return empty relations array when no relations exist', async () => { + const mockRelations = { + data: {}, + }; + mockPeopleAPI.people.get.mockResolvedValue(mockRelations); + + const result = await peopleService.getUserRelations({}); + + expect(JSON.parse(result.content[0].text)).toEqual({ + resourceName: 'people/me', + relations: [], + }); + }); + + it('should return empty array when filtering for non-existent relationType', async () => { + const mockRelations = { + data: { + relations: [ + { person: 'John Doe', type: 'manager' }, + ], + }, + }; + mockPeopleAPI.people.get.mockResolvedValue(mockRelations); + + const result = await peopleService.getUserRelations({ relationType: 'spouse' }); + + expect(JSON.parse(result.content[0].text)).toEqual({ + resourceName: 'people/me', + relationType: 'spouse', + relations: [], + }); + }); + + it('should handle errors during getUserRelations', async () => { + const apiError = new Error('API Error'); + mockPeopleAPI.people.get.mockRejectedValue(apiError); + + const result = await peopleService.getUserRelations({}); + + expect(JSON.parse(result.content[0].text)).toEqual({ error: 'API Error' }); + }); + + it('should call with the correct resourceName when a userId is provided', async () => { + const mockRelations = { + data: { + relations: [ + { person: 'John Doe', type: 'manager' }, + ], + }, + }; + mockPeopleAPI.people.get.mockResolvedValue(mockRelations); + + await peopleService.getUserRelations({ userId: '110001608645105799644' }); + + expect(mockPeopleAPI.people.get).toHaveBeenCalledWith({ + resourceName: 'people/110001608645105799644', + personFields: 'relations', + }); + }); + }); +}); \ No newline at end of file diff --git a/workspace-server/src/index.ts b/workspace-server/src/index.ts index 0cb5f62..b72ebaa 100644 --- a/workspace-server/src/index.ts +++ b/workspace-server/src/index.ts @@ -683,6 +683,18 @@ There are a list of system labels that can be modified on a message: peopleService.getMe ); + server.registerTool( + "people.getUserRelations", + { + description: 'Gets a user\'s relations (e.g., manager, spouse, assistant, etc.). Common relation types include: manager, assistant, spouse, partner, relative, mother, father, parent, sibling, child, friend, domesticPartner, referredBy. Defaults to the authenticated user if no userId is provided.', + inputSchema: { + userId: z.string().optional().describe('The ID of the user to get relations for (e.g., "110001608645105799644" or "people/110001608645105799644"). Defaults to the authenticated user if not provided.'), + relationType: z.string().optional().describe('The type of relation to filter by (e.g., "manager", "spouse", "assistant"). If not provided, returns all relations.'), + } + }, + peopleService.getUserRelations + ); + // 4. Connect the transport layer and start listening const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/workspace-server/src/services/PeopleService.ts b/workspace-server/src/services/PeopleService.ts index d66e39e..35fac5c 100644 --- a/workspace-server/src/services/PeopleService.ts +++ b/workspace-server/src/services/PeopleService.ts @@ -94,4 +94,58 @@ export class PeopleService { }; } } + + /** + * Gets a user's relations (e.g., manager, spouse, assistant). + * Defaults to the authenticated user if no userId is provided. + * Optionally filters by a specific relation type. + */ + public getUserRelations = async ({ userId, relationType }: { userId?: string, relationType?: string }) => { + const targetUser = userId ? (userId.startsWith('people/') ? userId : `people/${userId}`) : 'people/me'; + logToFile(`[PeopleService] Starting getUserRelations for ${targetUser} with relationType=${relationType}`); + try { + const people = await this.getPeopleClient(); + const res = await people.people.get({ + resourceName: targetUser, + personFields: 'relations', + }); + logToFile(`[PeopleService] Finished getUserRelations API call`); + + const relations = res.data?.relations || []; + + const filteredRelations = relationType + ? relations.filter( + (relation) => relation.type?.toLowerCase() === relationType.toLowerCase() + ) + : relations; + + if (relationType) { + logToFile(`[PeopleService] Filtered to ${filteredRelations.length} relations of type: ${relationType}`); + } else { + logToFile(`[PeopleService] Returning all ${filteredRelations.length} relations`); + } + + const responseData = { + resourceName: targetUser, + ...(relationType && { relationType }), + relations: filteredRelations, + }; + + return { + content: [{ + type: "text" as const, + text: JSON.stringify(responseData), + }], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[PeopleService] Error during people.getUserRelations: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } } \ No newline at end of file