diff options
Diffstat (limited to 'packages/server/src/core/client.ts')
| -rw-r--r-- | packages/server/src/core/client.ts | 265 |
1 files changed, 0 insertions, 265 deletions
diff --git a/packages/server/src/core/client.ts b/packages/server/src/core/client.ts deleted file mode 100644 index 9006c675..00000000 --- a/packages/server/src/core/client.ts +++ /dev/null @@ -1,265 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - GenerateContentConfig, - GoogleGenAI, - Part, - SchemaUnion, - PartListUnion, - Content, - Tool, -} from '@google/genai'; -import process from 'node:process'; -import { getFolderStructure } from '../utils/getFolderStructure.js'; -import { Turn, ServerGeminiStreamEvent } from './turn.js'; -import { Config } from '../config/config.js'; -import { getCoreSystemPrompt } from './prompts.js'; -import { ReadManyFilesTool } from '../tools/read-many-files.js'; -import { getResponseText } from '../utils/generateContentResponseUtilities.js'; -import { checkNextSpeaker } from '../utils/nextSpeakerChecker.js'; -import { reportError } from '../utils/errorReporting.js'; -import { GeminiChat } from './geminiChat.js'; -import { retryWithBackoff } from '../utils/retry.js'; - -export class GeminiClient { - private client: GoogleGenAI; - private model: string; - private generateContentConfig: GenerateContentConfig = { - temperature: 0, - topP: 1, - }; - private readonly MAX_TURNS = 100; - - constructor(private config: Config) { - const userAgent = config.getUserAgent(); - const apiKeyFromConfig = config.getApiKey(); - const vertexaiFlag = config.getVertexAI(); - - this.client = new GoogleGenAI({ - apiKey: apiKeyFromConfig === '' ? undefined : apiKeyFromConfig, - vertexai: vertexaiFlag, - httpOptions: { - headers: { - 'User-Agent': userAgent, - }, - }, - }); - this.model = config.getModel(); - } - - 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(); - - const initialParts: Part[] = [{ text: context }]; - - // Add full file context if the flag is set - if (this.config.getFullContext()) { - try { - const readManyFilesTool = this.config - .getToolRegistry() - .getTool('read_many_files') as ReadManyFilesTool; - if (readManyFilesTool) { - // Read all files in the target directory - const result = await readManyFilesTool.execute( - { - paths: ['**/*'], // Read everything recursively - useDefaultExcludes: true, // Use default excludes - }, - AbortSignal.timeout(30000), - ); - if (result.llmContent) { - initialParts.push({ - text: `\n--- Full File Context ---\n${result.llmContent}`, - }); - } else { - console.warn( - 'Full context requested, but read_many_files returned no content.', - ); - } - } else { - console.warn( - 'Full context requested, but read_many_files tool not found.', - ); - } - } catch (error) { - // Not using reportError here as it's a startup/config phase, not a chat/generation phase error. - console.error('Error reading full file context:', error); - initialParts.push({ - text: '\n--- Error reading full file context ---', - }); - } - } - - return initialParts; - } - - async startChat(): Promise<GeminiChat> { - const envParts = await this.getEnvironment(); - const toolDeclarations = this.config - .getToolRegistry() - .getFunctionDeclarations(); - const tools: Tool[] = [{ functionDeclarations: toolDeclarations }]; - const history: Content[] = [ - { - role: 'user', - parts: envParts, - }, - { - role: 'model', - parts: [{ text: 'Got it. Thanks for the context!' }], - }, - ]; - try { - const userMemory = this.config.getUserMemory(); - const systemInstruction = getCoreSystemPrompt(userMemory); - - return new GeminiChat( - this.client, - this.client.models, - this.model, - { - systemInstruction, - ...this.generateContentConfig, - tools, - }, - history, - ); - } catch (error) { - await reportError( - error, - 'Error initializing Gemini chat session.', - history, - 'startChat', - ); - const message = error instanceof Error ? error.message : 'Unknown error.'; - throw new Error(`Failed to initialize chat: ${message}`); - } - } - - async *sendMessageStream( - chat: GeminiChat, - request: PartListUnion, - signal: AbortSignal, - turns: number = this.MAX_TURNS, - ): AsyncGenerator<ServerGeminiStreamEvent> { - if (!turns) { - return; - } - - const turn = new Turn(chat); - const resultStream = turn.run(request, signal); - for await (const event of resultStream) { - yield event; - } - if (!turn.pendingToolCalls.length && signal && !signal.aborted) { - const nextSpeakerCheck = await checkNextSpeaker(chat, this, signal); - if (nextSpeakerCheck?.next_speaker === 'model') { - const nextRequest = [{ text: 'Please continue.' }]; - yield* this.sendMessageStream(chat, nextRequest, signal, turns - 1); - } - } - } - - async generateJson( - contents: Content[], - schema: SchemaUnion, - abortSignal: AbortSignal, - model: string = 'gemini-2.0-flash', - config: GenerateContentConfig = {}, - ): Promise<Record<string, unknown>> { - try { - const userMemory = this.config.getUserMemory(); - const systemInstruction = getCoreSystemPrompt(userMemory); - const requestConfig = { - abortSignal, - ...this.generateContentConfig, - ...config, - }; - - const apiCall = () => - this.client.models.generateContent({ - model, - config: { - ...requestConfig, - systemInstruction, - responseSchema: schema, - responseMimeType: 'application/json', - }, - contents, - }); - - const result = await retryWithBackoff(apiCall); - - const text = getResponseText(result); - if (!text) { - const error = new Error( - 'API returned an empty response for generateJson.', - ); - await reportError( - error, - 'Error in generateJson: API returned an empty response.', - contents, - 'generateJson-empty-response', - ); - throw error; - } - try { - return JSON.parse(text); - } catch (parseError) { - await reportError( - parseError, - 'Failed to parse JSON response from generateJson.', - { - responseTextFailedToParse: text, - originalRequestContents: contents, - }, - 'generateJson-parse', - ); - throw new Error( - `Failed to parse API response as JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`, - ); - } - } catch (error) { - if (abortSignal.aborted) { - // Regular cancellation error, fail normally - throw error; - } - - // Avoid double reporting for the empty response case handled above - if ( - error instanceof Error && - error.message === 'API returned an empty response for generateJson.' - ) { - throw error; - } - await reportError( - error, - 'Error generating JSON content via API.', - contents, - 'generateJson-api', - ); - const message = - error instanceof Error ? error.message : 'Unknown API error.'; - throw new Error(`Failed to generate JSON content: ${message}`); - } - } -} |
