Skip to content

Commit 414d914

Browse files
feat: enrich custom companies (#3311)
1 parent dc4cdd3 commit 414d914

12 files changed

Lines changed: 920 additions & 9 deletions

File tree

.infra/Pulumi.prod.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,7 @@ config:
196196
secure: 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
197197
gcp:project: devkit-prod
198198
gcp:region: us-central1
199+
api:anthropicApiKey:
200+
secure: AAABAHEzcSWbWl8xVDhOLgPCopvQnihNX6MIzyG/JbaYeGA3p1qrJ3UEQhOvBg7kMoIbx9u+0CRC102IldakBAlxx0l9l9kCDSE/JqqfCfjiLT5mjdLISRE5q2dQz+MaqqVzqm0MzaIQQkWBmOxeJAxRxQ/+/dWdla8RMWKG+Q7PnrH8vS8PeNKASRQ=
201+
api:anthropicApiUrl: https://api.anthropic.com/v1/messages
202+
api:anthropicVersion: 2023-06-01
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import nock from 'nock';
2+
import { AnthropicClient } from '../../../src/integrations/anthropic/client';
3+
import {
4+
GarmrService,
5+
GarmrNoopService,
6+
} from '../../../src/integrations/garmr';
7+
import type { AnthropicResponse } from '../../../src/integrations/anthropic/types';
8+
9+
describe('AnthropicClient', () => {
10+
const API_KEY = 'test-api-key';
11+
let client: AnthropicClient;
12+
13+
beforeAll(() => {
14+
process.env.ANTHROPIC_API_URL = 'https://api.anthropic.com/v1/messages';
15+
process.env.ANTHROPIC_VERSION = '2023-06-01';
16+
});
17+
18+
beforeEach(() => {
19+
nock.cleanAll();
20+
client = new AnthropicClient(API_KEY);
21+
});
22+
23+
afterEach(() => {
24+
nock.cleanAll();
25+
});
26+
27+
describe('constructor', () => {
28+
it('should use GarmrNoopService by default', () => {
29+
const defaultClient = new AnthropicClient(API_KEY);
30+
expect(defaultClient.garmr).toBeInstanceOf(GarmrNoopService);
31+
});
32+
33+
it('should use provided Garmr service', () => {
34+
const customGarmr = new GarmrService({
35+
service: 'test',
36+
breakerOpts: {
37+
halfOpenAfter: 10000,
38+
threshold: 0.2,
39+
duration: 30000,
40+
},
41+
retryOpts: {
42+
maxAttempts: 3,
43+
backoff: 500,
44+
},
45+
});
46+
47+
const customClient = new AnthropicClient(API_KEY, { garmr: customGarmr });
48+
expect(customClient.garmr).toBe(customGarmr);
49+
});
50+
});
51+
52+
describe('createMessage', () => {
53+
const mockResponse: AnthropicResponse = {
54+
content: [
55+
{
56+
input: {},
57+
},
58+
],
59+
};
60+
61+
it('should send request with correct headers', async () => {
62+
const scope = nock('https://api.anthropic.com')
63+
.post('/v1/messages')
64+
.matchHeader('Content-Type', 'application/json')
65+
.matchHeader('x-api-key', API_KEY)
66+
.matchHeader('anthropic-version', '2023-06-01')
67+
.reply(200, mockResponse);
68+
69+
await client.createMessage({
70+
model: 'claude-sonnet-4-5-20250929',
71+
max_tokens: 1024,
72+
system: 'You are a helpful assistant.',
73+
messages: [{ role: 'user', content: 'Hello' }],
74+
});
75+
76+
expect(scope.isDone()).toBe(true);
77+
});
78+
79+
it('should return parsed response on success', async () => {
80+
nock('https://api.anthropic.com')
81+
.post('/v1/messages')
82+
.reply(200, mockResponse);
83+
84+
const result = await client.createMessage({
85+
model: 'claude-sonnet-4-5-20250929',
86+
max_tokens: 1024,
87+
system: 'You are a helpful assistant.',
88+
messages: [{ role: 'user', content: 'Hello' }],
89+
});
90+
91+
expect(result).toEqual(mockResponse);
92+
});
93+
94+
it('should handle tool use response', async () => {
95+
const toolUseResponse: AnthropicResponse = {
96+
content: [
97+
{
98+
input: {
99+
englishName: 'Google',
100+
nativeName: 'Google',
101+
domain: 'google.com',
102+
},
103+
},
104+
],
105+
};
106+
107+
nock('https://api.anthropic.com')
108+
.post('/v1/messages')
109+
.reply(200, toolUseResponse);
110+
111+
const result = await client.createMessage({
112+
model: 'claude-sonnet-4-5-20250929',
113+
max_tokens: 1024,
114+
system: 'You are a helpful assistant.',
115+
messages: [{ role: 'user', content: 'Google' }],
116+
tools: [
117+
{
118+
name: 'organization_info',
119+
description: 'Gets information about the given organization',
120+
input_schema: {
121+
type: 'object',
122+
properties: {
123+
englishName: { type: 'string' },
124+
nativeName: { type: 'string' },
125+
domain: { type: 'string' },
126+
},
127+
required: ['englishName', 'nativeName', 'domain'],
128+
},
129+
},
130+
],
131+
tool_choice: {
132+
type: 'tool',
133+
name: 'organization_info',
134+
},
135+
});
136+
137+
expect(result.content[0].input).toEqual({
138+
englishName: 'Google',
139+
nativeName: 'Google',
140+
domain: 'google.com',
141+
});
142+
});
143+
144+
it('should throw error on API failure', async () => {
145+
nock('https://api.anthropic.com')
146+
.post('/v1/messages')
147+
.reply(400, 'Bad Request');
148+
149+
await expect(
150+
client.createMessage({
151+
model: 'claude-sonnet-4-5-20250929',
152+
max_tokens: 1024,
153+
system: 'You are a helpful assistant.',
154+
messages: [{ role: 'user', content: 'Hello' }],
155+
}),
156+
).rejects.toThrow('Anthropic API error: 400 Bad Request - Bad Request');
157+
});
158+
159+
it('should throw error on 401 unauthorized', async () => {
160+
nock('https://api.anthropic.com')
161+
.post('/v1/messages')
162+
.reply(401, 'Unauthorized');
163+
164+
await expect(
165+
client.createMessage({
166+
model: 'claude-sonnet-4-5-20250929',
167+
max_tokens: 1024,
168+
system: 'You are a helpful assistant.',
169+
messages: [{ role: 'user', content: 'Hello' }],
170+
}),
171+
).rejects.toThrow('Anthropic API error: 401 Unauthorized - Unauthorized');
172+
});
173+
174+
it('should throw error on 429 rate limit', async () => {
175+
nock('https://api.anthropic.com')
176+
.post('/v1/messages')
177+
.reply(429, 'Rate limit exceeded');
178+
179+
await expect(
180+
client.createMessage({
181+
model: 'claude-sonnet-4-5-20250929',
182+
max_tokens: 1024,
183+
system: 'You are a helpful assistant.',
184+
messages: [{ role: 'user', content: 'Hello' }],
185+
}),
186+
).rejects.toThrow(
187+
'Anthropic API error: 429 Too Many Requests - Rate limit exceeded',
188+
);
189+
});
190+
191+
it('should throw error on 500 server error', async () => {
192+
nock('https://api.anthropic.com')
193+
.post('/v1/messages')
194+
.reply(500, 'Internal Server Error');
195+
196+
await expect(
197+
client.createMessage({
198+
model: 'claude-sonnet-4-5-20250929',
199+
max_tokens: 1024,
200+
system: 'You are a helpful assistant.',
201+
messages: [{ role: 'user', content: 'Hello' }],
202+
}),
203+
).rejects.toThrow(
204+
'Anthropic API error: 500 Internal Server Error - Internal Server Error',
205+
);
206+
});
207+
208+
it('should include error details in error message', async () => {
209+
const errorResponse = JSON.stringify({
210+
error: {
211+
type: 'invalid_request_error',
212+
message: 'max_tokens must be greater than 0',
213+
},
214+
});
215+
216+
nock('https://api.anthropic.com')
217+
.post('/v1/messages')
218+
.reply(400, errorResponse);
219+
220+
await expect(
221+
client.createMessage({
222+
model: 'claude-sonnet-4-5-20250929',
223+
max_tokens: 0,
224+
system: 'You are a helpful assistant.',
225+
messages: [{ role: 'user', content: 'Hello' }],
226+
}),
227+
).rejects.toThrow(errorResponse);
228+
});
229+
});
230+
});

__tests__/schema/autocompletes.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,22 @@ describe('query autocompleteCompany', () => {
861861
domains: ['amazon.com'],
862862
type: CompanyType.Company,
863863
},
864+
{
865+
id: 'samsung',
866+
name: 'Samsung Electronics',
867+
altName: '삼성전자',
868+
image: 'https://example.com/samsung.png',
869+
domains: ['samsung.com'],
870+
type: CompanyType.Company,
871+
},
872+
{
873+
id: 'toyota',
874+
name: 'Toyota Motor Corporation',
875+
altName: 'トヨタ自動車',
876+
image: 'https://example.com/toyota.png',
877+
domains: ['toyota.com'],
878+
type: CompanyType.Company,
879+
},
864880
{
865881
id: 'mit',
866882
name: 'Massachusetts Institute of Technology',
@@ -889,6 +905,14 @@ describe('query autocompleteCompany', () => {
889905
domains: ['berkeley.edu'],
890906
type: CompanyType.School,
891907
},
908+
{
909+
id: 'todai',
910+
name: 'The University of Tokyo',
911+
altName: '東京大学',
912+
image: 'https://example.com/todai.png',
913+
domains: ['u-tokyo.ac.jp'],
914+
type: CompanyType.School,
915+
},
892916
]);
893917
});
894918

@@ -939,6 +963,11 @@ describe('query autocompleteCompany', () => {
939963
name: 'Stanford University',
940964
image: 'https://example.com/stanford.png',
941965
},
966+
{
967+
id: 'todai',
968+
name: 'The University of Tokyo',
969+
image: 'https://example.com/todai.png',
970+
},
942971
{
943972
id: 'berkeley',
944973
name: 'University of California, Berkeley',
@@ -976,6 +1005,16 @@ describe('query autocompleteCompany', () => {
9761005
name: 'Microsoft Corporation',
9771006
image: 'https://example.com/microsoft.png',
9781007
},
1008+
{
1009+
id: 'samsung',
1010+
name: 'Samsung Electronics',
1011+
image: 'https://example.com/samsung.png',
1012+
},
1013+
{
1014+
id: 'toyota',
1015+
name: 'Toyota Motor Corporation',
1016+
image: 'https://example.com/toyota.png',
1017+
},
9791018
]);
9801019
});
9811020

@@ -1050,11 +1089,21 @@ describe('query autocompleteCompany', () => {
10501089
name: 'Microsoft Corporation',
10511090
image: 'https://example.com/microsoft.png',
10521091
},
1092+
{
1093+
id: 'samsung',
1094+
name: 'Samsung Electronics',
1095+
image: 'https://example.com/samsung.png',
1096+
},
10531097
{
10541098
id: 'stanford',
10551099
name: 'Stanford University',
10561100
image: 'https://example.com/stanford.png',
10571101
},
1102+
{
1103+
id: 'toyota',
1104+
name: 'Toyota Motor Corporation',
1105+
image: 'https://example.com/toyota.png',
1106+
},
10581107
{
10591108
id: 'berkeley',
10601109
name: 'University of California, Berkeley',
@@ -1187,4 +1236,60 @@ describe('query autocompleteCompany', () => {
11871236
},
11881237
]);
11891238
});
1239+
1240+
it('should return results when searching by English name for company with altName', async () => {
1241+
const res = await client.query(QUERY, {
1242+
variables: { query: 'samsung' },
1243+
});
1244+
1245+
expect(res.errors).toBeFalsy();
1246+
expect(res.data.autocompleteCompany).toMatchObject([
1247+
{
1248+
id: 'samsung',
1249+
name: 'Samsung Electronics',
1250+
image: 'https://example.com/samsung.png',
1251+
},
1252+
]);
1253+
});
1254+
1255+
it('should return results when searching by non-Latin characters (Korean)', async () => {
1256+
const res = await client.query(QUERY, {
1257+
variables: { query: '삼성' },
1258+
});
1259+
1260+
expect(res.errors).toBeFalsy();
1261+
expect(res.data.autocompleteCompany).toMatchObject([
1262+
{
1263+
id: 'samsung',
1264+
name: 'Samsung Electronics',
1265+
image: 'https://example.com/samsung.png',
1266+
},
1267+
]);
1268+
});
1269+
1270+
it('should return results when searching by non-Latin characters (Japanese)', async () => {
1271+
const res = await client.query(QUERY, {
1272+
variables: { query: 'トヨタ' },
1273+
});
1274+
1275+
expect(res.errors).toBeFalsy();
1276+
expect(res.data.autocompleteCompany).toMatchObject([
1277+
{
1278+
id: 'toyota',
1279+
name: 'Toyota Motor Corporation',
1280+
image: 'https://example.com/toyota.png',
1281+
},
1282+
]);
1283+
});
1284+
1285+
it('should not return unrelated companies when searching with non-Latin characters', async () => {
1286+
const res = await client.query(QUERY, {
1287+
variables: { query: '삼성' },
1288+
});
1289+
1290+
expect(res.errors).toBeFalsy();
1291+
// Should only return Samsung, not other companies with non-Latin altNames
1292+
expect(res.data.autocompleteCompany.length).toBe(1);
1293+
expect(res.data.autocompleteCompany[0].id).toBe('samsung');
1294+
});
11901295
});

0 commit comments

Comments
 (0)