Skip to content

Commit bbb299d

Browse files
committed
feat: Support Gemini via Vertex AI
1 parent ac5766e commit bbb299d

File tree

7 files changed

+231
-13
lines changed

7 files changed

+231
-13
lines changed

packages/cli/src/cmds/index/aiEnvVar.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
export const AI_KEY_ENV_VARS = ['OPENAI_API_KEY', 'AZURE_OPENAI_API_KEY', 'ANTHROPIC_API_KEY'];
1+
export const AI_KEY_ENV_VARS = [
2+
'GOOGLE_WEB_CREDENTIALS',
3+
'OPENAI_API_KEY',
4+
'AZURE_OPENAI_API_KEY',
5+
'ANTHROPIC_API_KEY',
6+
];
27

38
export default function detectAIEnvVar(): string | undefined {
49
return Object.keys(process.env).find((key) => AI_KEY_ENV_VARS.includes(key));

packages/cli/src/cmds/index/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ import LocalNavie from '../../rpc/explain/navie/navie-local';
2424
import RemoteNavie from '../../rpc/explain/navie/navie-remote';
2525
import { InteractionEvent } from '@appland/navie/dist/interaction-history';
2626
import { update } from '../../rpc/file/update';
27-
28-
const AI_KEY_ENV_VARS = ['OPENAI_API_KEY', 'AZURE_OPENAI_API_KEY'];
27+
import { AI_KEY_ENV_VARS } from './aiEnvVar';
2928

3029
export const command = 'index';
3130
export const describe =

packages/cli/src/rpc/llmConfiguration.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,14 @@ function openAIBaseURL(): string | undefined {
4747
return baseUrl;
4848
}
4949

50+
const DEFAULT_BASE_URLS = {
51+
anthropic: 'https://api.anthropic.com/v1/',
52+
'vertex-ai': 'https://googleapis.com',
53+
openai: undefined,
54+
} as const;
55+
5056
export function getLLMConfiguration(): LLMConfiguration {
51-
const baseUrl =
52-
SELECTED_BACKEND === 'anthropic' ? 'https://api.anthropic.com/v1/' : openAIBaseURL();
57+
const baseUrl = (SELECTED_BACKEND && DEFAULT_BASE_URLS[SELECTED_BACKEND]) ?? openAIBaseURL();
5358

5459
return {
5560
baseUrl,

packages/navie/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"dependencies": {
4242
"@langchain/anthropic": "^0.3.1",
4343
"@langchain/core": "^0.2.27",
44+
"@langchain/google-vertexai-web": "^0.1.0",
4445
"@langchain/openai": "^0.2.7",
4546
"fast-xml-parser": "^4.4.0",
4647
"js-yaml": "^4.1.0",
Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { warn } from 'node:console';
22

3+
import GoogleVertexAICompletionService from './google-vertexai-completion-service';
34
import OpenAICompletionService from './openai-completion-service';
45
import AnthropicCompletionService from './anthropic-completion-service';
56
import CompletionService from './completion-service';
@@ -11,34 +12,43 @@ interface Options {
1112
trajectory: Trajectory;
1213
}
1314

14-
type Backend = 'anthropic' | 'openai';
15+
const BACKENDS = {
16+
anthropic: AnthropicCompletionService,
17+
openai: OpenAICompletionService,
18+
'vertex-ai': GoogleVertexAICompletionService,
19+
} as const;
1520

16-
function defaultBackend(): Backend {
17-
return 'ANTHROPIC_API_KEY' in process.env ? 'anthropic' : 'openai';
21+
type Backend = keyof typeof BACKENDS;
22+
23+
function defaultBackend(): Backend | undefined {
24+
if ('ANTHROPIC_API_KEY' in process.env) return 'anthropic';
25+
if ('GOOGLE_WEB_CREDENTIALS' in process.env) return 'vertex-ai';
26+
if ('OPENAI_API_KEY' in process.env) return 'openai';
1827
}
1928

2029
function environmentBackend(): Backend | undefined {
2130
switch (process.env.APPMAP_NAVIE_COMPLETION_BACKEND) {
2231
case 'anthropic':
2332
case 'openai':
33+
case 'vertex-ai':
2434
return process.env.APPMAP_NAVIE_COMPLETION_BACKEND;
2535
default:
2636
return undefined;
2737
}
2838
}
2939

30-
export const SELECTED_BACKEND: Backend = environmentBackend() ?? defaultBackend();
40+
export const SELECTED_BACKEND: Backend | undefined = environmentBackend() ?? defaultBackend();
3141

3242
export default function createCompletionService({
3343
modelName,
3444
temperature,
3545
trajectory,
3646
}: Options): CompletionService {
3747
const backend = environmentBackend() ?? defaultBackend();
38-
if (backend === 'anthropic') {
39-
warn('Using Anthropic AI backend');
40-
return new AnthropicCompletionService(modelName, temperature, trajectory);
48+
if (backend && backend in BACKENDS) {
49+
warn(`Using completion service ${backend}`);
50+
return new BACKENDS[backend](modelName, temperature, trajectory);
4151
}
42-
warn('Using OpenAI backend');
52+
warn(`No completion service available for backend ${backend}. Falling back to OpenAI.`);
4353
return new OpenAICompletionService(modelName, temperature, trajectory);
4454
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { warn } from 'node:console';
2+
import { isNativeError } from 'node:util/types';
3+
4+
import type { ChatVertexAI, ChatVertexAIInput } from '@langchain/google-vertexai-web';
5+
import { zodResponseFormat } from 'openai/helpers/zod';
6+
import { z } from 'zod';
7+
8+
import Trajectory from '../lib/trajectory';
9+
import Message from '../message';
10+
import CompletionService, {
11+
CompleteOptions,
12+
Completion,
13+
CompletionRetries,
14+
CompletionRetryDelay,
15+
convertToMessage,
16+
mergeSystemMessages,
17+
Usage,
18+
} from './completion-service';
19+
20+
const VertexAI = import('@langchain/google-vertexai-web');
21+
22+
export default class GoogleVertexAICompletionService implements CompletionService {
23+
constructor(
24+
public readonly modelName: string,
25+
public readonly temperature: number,
26+
private trajectory: Trajectory
27+
) {}
28+
29+
// Construct a model with non-default options. There doesn't seem to be a way to configure
30+
// the model parameters at invocation time like with OpenAI.
31+
private async buildModel(options?: ChatVertexAIInput): Promise<ChatVertexAI> {
32+
return new (await VertexAI).ChatVertexAI({
33+
model: this.modelName,
34+
temperature: this.temperature,
35+
streaming: true,
36+
...options,
37+
});
38+
}
39+
40+
get miniModelName(): string {
41+
const miniModel = process.env.APPMAP_NAVIE_MINI_MODEL;
42+
return miniModel ?? 'gemini-1.5-flash-002';
43+
}
44+
45+
// Request a JSON object with a given JSON schema.
46+
async json<Schema extends z.ZodType>(
47+
messages: Message[],
48+
schema: Schema,
49+
options?: CompleteOptions
50+
): Promise<z.infer<Schema> | undefined> {
51+
const model = await this.buildModel({
52+
...options,
53+
streaming: false,
54+
responseMimeType: 'application/json',
55+
});
56+
const sentMessages = mergeSystemMessages([
57+
...messages,
58+
{
59+
role: 'system',
60+
content: `Use the following JSON schema for your response:\n\n${JSON.stringify(
61+
zodResponseFormat(schema, 'requestedObject').json_schema.schema,
62+
null,
63+
2
64+
)}`,
65+
},
66+
]);
67+
68+
for (const message of sentMessages) this.trajectory.logSentMessage(message);
69+
70+
const response = await model.invoke(sentMessages.map(convertToMessage));
71+
72+
this.trajectory.logReceivedMessage({
73+
role: 'assistant',
74+
content: JSON.stringify(response),
75+
});
76+
77+
const sanitizedContent = response.content.toString().replace(/^`{3,}[^\s]*?$/gm, '');
78+
const parsed = JSON.parse(sanitizedContent) as unknown;
79+
schema.parse(parsed);
80+
return parsed;
81+
}
82+
83+
async *complete(messages: readonly Message[], options?: { temperature?: number }): Completion {
84+
const usage = new Usage();
85+
const model = await this.buildModel(options);
86+
const sentMessages: Message[] = mergeSystemMessages(messages);
87+
const tokens = new Array<string>();
88+
for (const message of sentMessages) this.trajectory.logSentMessage(message);
89+
90+
const maxAttempts = CompletionRetries;
91+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
92+
try {
93+
// eslint-disable-next-line no-await-in-loop
94+
const response = await model.stream(sentMessages.map(convertToMessage));
95+
96+
// eslint-disable-next-line @typescript-eslint/naming-convention, no-await-in-loop
97+
for await (const { content, usage_metadata } of response) {
98+
yield content.toString();
99+
tokens.push(content.toString());
100+
if (usage_metadata) {
101+
usage.promptTokens += usage_metadata.input_tokens;
102+
usage.completionTokens += usage_metadata.output_tokens;
103+
}
104+
}
105+
106+
this.trajectory.logReceivedMessage({
107+
role: 'assistant',
108+
content: tokens.join(''),
109+
});
110+
111+
break;
112+
} catch (cause) {
113+
if (attempt < maxAttempts - 1 && tokens.length === 0) {
114+
const nextAttempt = CompletionRetryDelay * 2 ** attempt;
115+
warn(`Received ${JSON.stringify(cause)}, retrying in ${nextAttempt}ms`);
116+
await new Promise<void>((resolve) => {
117+
setTimeout(resolve, nextAttempt);
118+
});
119+
continue;
120+
}
121+
throw new Error(
122+
`Failed to complete after ${attempt + 1} attempt(s): ${errorMessage(cause)}`,
123+
{
124+
cause,
125+
}
126+
);
127+
}
128+
}
129+
130+
warn(usage.toString());
131+
return usage;
132+
}
133+
}
134+
135+
function errorMessage(err: unknown): string {
136+
if (isNativeError(err)) return err.cause ? errorMessage(err.cause) : err.message;
137+
return String(err);
138+
}

yarn.lock

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ __metadata:
456456
dependencies:
457457
"@langchain/anthropic": ^0.3.1
458458
"@langchain/core": ^0.2.27
459+
"@langchain/google-vertexai-web": ^0.1.0
459460
"@langchain/openai": ^0.2.7
460461
"@tsconfig/node-lts": ^20.1.3
461462
"@types/jest": ^29.4.1
@@ -7038,6 +7039,41 @@ __metadata:
70387039
languageName: node
70397040
linkType: hard
70407041

7042+
"@langchain/google-common@npm:~0.1.0":
7043+
version: 0.1.1
7044+
resolution: "@langchain/google-common@npm:0.1.1"
7045+
dependencies:
7046+
uuid: ^10.0.0
7047+
zod-to-json-schema: ^3.22.4
7048+
peerDependencies:
7049+
"@langchain/core": ">=0.2.21 <0.4.0"
7050+
checksum: e460a08eaf5e6902c3cb7e8deb9edddcdb46c6bc38657ee1050d05ab5f17bf864bf298a9f00cc41e2824f8c072d79c1dca9b84a7ce64ebcf5a5357af14f5b9d9
7051+
languageName: node
7052+
linkType: hard
7053+
7054+
"@langchain/google-vertexai-web@npm:^0.1.0":
7055+
version: 0.1.0
7056+
resolution: "@langchain/google-vertexai-web@npm:0.1.0"
7057+
dependencies:
7058+
"@langchain/google-webauth": ~0.1.0
7059+
peerDependencies:
7060+
"@langchain/core": ">=0.2.21 <0.4.0"
7061+
checksum: 8c32499e4070ddf28de26e3e4354c60303921e0be84aa68bbcbbeecd5e79e78354fb940708dcfc94efbc67f51893e51039288d78418ec00ec3f64a6cb1e5b20e
7062+
languageName: node
7063+
linkType: hard
7064+
7065+
"@langchain/google-webauth@npm:~0.1.0":
7066+
version: 0.1.0
7067+
resolution: "@langchain/google-webauth@npm:0.1.0"
7068+
dependencies:
7069+
"@langchain/google-common": ~0.1.0
7070+
web-auth-library: ^1.0.3
7071+
peerDependencies:
7072+
"@langchain/core": ">=0.2.21 <0.4.0"
7073+
checksum: 90d7c04f95e9950ec5fb39a779352f145efa319d2003564b82a183809ef92d64f8f878999e5cb9c75b1bfda83e38c9b650946c928b1d137dd8bf0bebbaddca74
7074+
languageName: node
7075+
linkType: hard
7076+
70417077
"@langchain/openai@npm:>=0.1.0 <0.3.0, @langchain/openai@npm:^0.2.7":
70427078
version: 0.2.7
70437079
resolution: "@langchain/openai@npm:0.2.7"
@@ -28406,6 +28442,13 @@ __metadata:
2840628442
languageName: node
2840728443
linkType: hard
2840828444

28445+
"jose@npm:>= 4.12.0 < 5.0.0":
28446+
version: 4.15.9
28447+
resolution: "jose@npm:4.15.9"
28448+
checksum: 41abe1c99baa3cf8a78ebbf93da8f8e50e417b7a26754c4afa21865d87527b8ac2baf66de2c5f6accc3f7d7158658dae7364043677236ea1d07895b040097f15
28449+
languageName: node
28450+
linkType: hard
28451+
2840928452
"joycon@npm:^3.0.1":
2841028453
version: 3.1.1
2841128454
resolution: "joycon@npm:3.1.1"
@@ -36925,6 +36968,13 @@ resolve@1.1.7:
3692536968
languageName: node
3692636969
linkType: hard
3692736970

36971+
"rfc4648@npm:^1.5.2":
36972+
version: 1.5.3
36973+
resolution: "rfc4648@npm:1.5.3"
36974+
checksum: 19c81d502582e377125b00fbd7a5cdb0e351f9a1e40182fa9f608b48e1ab852d211b75facb2f4f3fa17f7c6ebc2ef4acca61ae7eb7fbcfa4768f11d2db678116
36975+
languageName: node
36976+
linkType: hard
36977+
3692836978
"rfdc@npm:^1.3.0":
3692936979
version: 1.3.0
3693036980
resolution: "rfdc@npm:1.3.0"
@@ -41838,6 +41888,16 @@ typescript@~4.4.3:
4183841888
languageName: node
4183941889
linkType: hard
4184041890

41891+
"web-auth-library@npm:^1.0.3":
41892+
version: 1.0.3
41893+
resolution: "web-auth-library@npm:1.0.3"
41894+
dependencies:
41895+
jose: ">= 4.12.0 < 5.0.0"
41896+
rfc4648: ^1.5.2
41897+
checksum: 9e2b303a43ac040037b952c8130260edd44c34956e34277e28faa23ebf63fc40ba3a22b7a3cab5f36ea6f7aac16573415ccacb6e5a970fe4f4d2ee5751ae01d1
41898+
languageName: node
41899+
linkType: hard
41900+
4184141901
"web-streams-polyfill@npm:4.0.0-beta.3":
4184241902
version: 4.0.0-beta.3
4184341903
resolution: "web-streams-polyfill@npm:4.0.0-beta.3"

0 commit comments

Comments
 (0)