diff options
| author | Evan Senter <[email protected]> | 2025-04-19 19:45:42 +0100 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-04-19 19:45:42 +0100 |
| commit | 3fce6cea27d3e6129d6c06e528b62e1b11bf7094 (patch) | |
| tree | 244b8e9ab94f902d65d4bda8739a6538e377ed17 /packages/server/src/core/gemini-client.ts | |
| parent | 0c9e1ef61be7db53e6e73b7208b649cd8cbed6c3 (diff) | |
Starting to modularize into separate cli / server packages. (#55)
* Starting to move a lot of code into packages/server
* More of the massive refactor, builds and runs, some issues though.
* Fixing outstanding issue with double messages.
* Fixing a minor UI issue.
* Fixing the build post-merge.
* Running formatting.
* Addressing comments.
Diffstat (limited to 'packages/server/src/core/gemini-client.ts')
| -rw-r--r-- | packages/server/src/core/gemini-client.ts | 171 |
1 files changed, 171 insertions, 0 deletions
diff --git a/packages/server/src/core/gemini-client.ts b/packages/server/src/core/gemini-client.ts new file mode 100644 index 00000000..c7415ed8 --- /dev/null +++ b/packages/server/src/core/gemini-client.ts @@ -0,0 +1,171 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + GenerateContentConfig, + GoogleGenAI, + Part, + Chat, + SchemaUnion, + PartListUnion, + Content, + FunctionDeclaration, + Tool, +} from '@google/genai'; +import { CoreSystemPrompt } from './prompts.js'; +import process from 'node:process'; +import { getFolderStructure } from '../utils/getFolderStructure.js'; +import { Turn, ServerTool, GeminiEventType } from './turn.js'; + +// Import the ServerGeminiStreamEvent type +type ServerGeminiStreamEvent = + | { type: GeminiEventType.Content; value: string } + | { + type: GeminiEventType.ToolCallRequest; + value: { callId: string; name: string; args: Record<string, unknown> }; + }; + +export class GeminiClient { + private ai: GoogleGenAI; + private model: string; + private generateContentConfig: GenerateContentConfig = { + temperature: 0, + topP: 1, + }; + private readonly MAX_TURNS = 100; + + constructor(apiKey: string, model: string) { + this.ai = new GoogleGenAI({ apiKey: apiKey }); + this.model = model; + } + + private async getEnvironment(): Promise<Part> { + const cwd = process.cwd(); + const today = new Date().toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + const platform = process.platform; + const folderStructure = await getFolderStructure(cwd); + const context = ` + Okay, just setting up the context for our chat. + Today is ${today}. + My operating system is: ${platform} + I'm currently working in the directory: ${cwd} + ${folderStructure} + `.trim(); + return { text: context }; + } + + async startChat(toolDeclarations: FunctionDeclaration[]): Promise<Chat> { + const envPart = await this.getEnvironment(); + const tools: Tool[] = toolDeclarations.map((declaration) => ({ + functionDeclarations: [declaration], + })); + try { + const chat = this.ai.chats.create({ + model: this.model, + config: { + systemInstruction: CoreSystemPrompt, + ...this.generateContentConfig, + tools: tools, + }, + history: [ + { + role: 'user', + parts: [envPart], + }, + { + role: 'model', + parts: [{ text: 'Got it. Thanks for the context!' }], + }, + ], + }); + return chat; + } catch (error) { + console.error('Error initializing Gemini chat session:', error); + const message = error instanceof Error ? error.message : 'Unknown error.'; + throw new Error(`Failed to initialize chat: ${message}`); + } + } + + async *sendMessageStream( + chat: Chat, + request: PartListUnion, + availableTools: ServerTool[], + signal?: AbortSignal, + ): AsyncGenerator<ServerGeminiStreamEvent> { + let turns = 0; + try { + while (turns < this.MAX_TURNS) { + turns++; + const turn = new Turn(chat, availableTools); + const resultStream = turn.run(request, signal); + for await (const event of resultStream) { + yield event; + } + const fnResponses = turn.getFunctionResponses(); + if (fnResponses.length > 0) { + request = fnResponses; + continue; + } else { + break; + } + } + if (turns >= this.MAX_TURNS) { + console.warn( + 'sendMessageStream: Reached maximum tool call turns limit.', + ); + } + } catch (error: unknown) { + if (error instanceof Error && error.name === 'AbortError') { + console.log('Gemini stream request aborted by user.'); + throw error; + } else { + console.error(`Error during Gemini stream or tool interaction:`, error); + throw error; + } + } + } + + async generateJson( + contents: Content[], + schema: SchemaUnion, + ): Promise<Record<string, unknown>> { + try { + const result = await this.ai.models.generateContent({ + model: this.model, + config: { + ...this.generateContentConfig, + systemInstruction: CoreSystemPrompt, + responseSchema: schema, + responseMimeType: 'application/json', + }, + contents, + }); + const responseText = result.text; + if (!responseText) { + throw new Error('API returned an empty response.'); + } + try { + const parsedJson = JSON.parse(responseText); + return parsedJson; + } catch (parseError) { + console.error('Failed to parse JSON response:', responseText); + throw new Error( + `Failed to parse API response as JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`, + ); + } + } catch (error) { + console.error('Error generating JSON content:', error); + const message = + error instanceof Error ? error.message : 'Unknown API error.'; + throw new Error(`Failed to generate JSON content: ${message}`); + } + } +} |
