Skip to content

Commit 30ffa6c

Browse files
committed
3.6.8, Gemini support. Fixed linting issues. Added Gemini switches to generationServiceFactory. Added gemini type to ApiType.ts. Removed default reset that was still causing infinite loops because of how the settings are loaded. Added gemini defaults to constants.ts
1 parent bb267cb commit 30ffa6c

15 files changed

+152
-81
lines changed

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"@aws-sdk/credential-provider-node": "^3.501.0",
6767
"@emotion/react": "^11.11.0",
6868
"@emotion/styled": "^11.11.0",
69+
"@google/generative-ai": "^0.21.0",
6970
"@mui/icons-material": "^5.11.16",
7071
"@mui/lab": "^5.0.0-alpha.132",
7172
"@mui/material": "^5.13.2",
@@ -76,7 +77,6 @@
7677
"@patternfly/react-styles": "^5.1.1",
7778
"@tanstack/react-query": "^5.18.1",
7879
"@widgetbot/react-embed": "^1.7.0",
79-
"@google/generative-ai": "^0.21.0",
8080
"adm-zip": "^0.5.10",
8181
"aws-amplify": "^5.3.6",
8282
"electron-debug": "^3.2.0",

release/app/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

release/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sentient-sims-app",
3-
"version": "3.6.7",
3+
"version": "3.6.8",
44
"description": "Companion app for the Sentient Sims mod",
55
"license": "MIT",
66
"author": {

src/main/sentient-sims/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,6 @@ export const novelaiDefaultModel = 'kayra-v1';
7070
export const sentientSimsAIDefaultModel = 'Gryphe/MythoMax-L2-13b';
7171
export const tokenizerBreakString = '<<BREAK>>';
7272
export const defaultWantsPrefixes = ['I want to', 'I would like', 'I feel'];
73+
export const defaultGeminiModel = 'gemini-2.0-flash-exp';
74+
export const geminiDefaultEndpoint =
75+
'https://generativelanguage.googleapis.com/v1beta';
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export class GeminiAPIError extends Error {
2+
constructor(public status: number, message: string) {
3+
super(message);
4+
this.name = 'GeminiAPIError';
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export class GeminiKeysNotSetError extends Error {
2+
constructor(message: string) {
3+
super(message);
4+
this.name = 'GeminiKeysNotSetError';
5+
}
6+
}

src/main/sentient-sims/factories/generationServiceFactory.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { NovelAITokenCounter } from '../tokens/NovelAITokenCounter';
1212
import { KoboldAIService } from '../services/KoboldAIService';
1313
import { AllModelSettings, ModelSettings } from '../modelSettings';
1414
import { stringType } from '../util/typeChecks';
15+
import { GeminiService } from '../services/GeminiService';
1516

1617
export function getGenerationService(
1718
settingsService: SettingsService
@@ -29,6 +30,10 @@ export function getGenerationService(
2930
return new NovelAIService(settingsService);
3031
}
3132

33+
if (aiType === ApiType.Gemini) {
34+
return new GeminiService(settingsService);
35+
}
36+
3237
return new OpenAIService(settingsService);
3338
}
3439

@@ -64,6 +69,10 @@ export function getModelSettings(
6469
model = settingsService.get(SettingsEnum.SENTIENTSIMSAI_MODEL);
6570
}
6671

72+
if (aiType === ApiType.Gemini) {
73+
model = settingsService.get(SettingsEnum.GEMINI_MODEL);
74+
}
75+
6776
if (stringType(model) && model in AllModelSettings) {
6877
modelSettings = AllModelSettings[model];
6978
}

src/main/sentient-sims/models/ApiType.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ export enum ApiType {
44
CustomAI = 'customai',
55
NovelAI = 'novelai',
66
KoboldAI = 'koboldai',
7+
Gemini = 'gemini',
78
}
89

9-
export function ApiTypeFromValue(value: any): ApiType {
10+
export function ApiTypeFromValue(value: any): ApiType | null {
1011
switch (value) {
1112
case ApiType.SentientSimsAI:
1213
return ApiType.SentientSimsAI;
@@ -16,12 +17,14 @@ export function ApiTypeFromValue(value: any): ApiType {
1617
return ApiType.KoboldAI;
1718
case ApiType.NovelAI:
1819
return ApiType.NovelAI;
20+
case ApiType.Gemini:
21+
return ApiType.Gemini;
1922
default:
2023
return ApiType.OpenAI;
2124
}
2225
}
2326

24-
export function ApiTypeName(apiType: ApiType): string {
27+
export function ApiTypeName(apiType: ApiType | null): string {
2528
switch (apiType) {
2629
case ApiType.SentientSimsAI:
2730
return 'Sentient Sims AI';
@@ -31,6 +34,8 @@ export function ApiTypeName(apiType: ApiType): string {
3134
return 'Novel AI';
3235
case ApiType.OpenAI:
3336
return 'OpenAI';
37+
case ApiType.Gemini:
38+
return 'Gemini';
3439
default:
3540
return 'AI';
3641
}

src/main/sentient-sims/services/GeminiService.ts

Lines changed: 88 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import log from 'electron-log';
2-
import { GoogleGenerativeAI, HarmBlockThreshold, HarmCategory } from '@google/generative-ai';
2+
import {
3+
GoogleGenerativeAI,
4+
HarmBlockThreshold,
5+
HarmCategory,
6+
} from '@google/generative-ai';
37
import { SettingsService } from './SettingsService';
48
import { SettingsEnum } from '../models/SettingsEnum';
59
import { GenerationService } from './GenerationService';
@@ -8,24 +12,11 @@ import { OpenAICompatibleRequest } from '../models/OpenAICompatibleRequest';
812
import { AIModel } from '../models/AIModel';
913
import { fetchWithRetries } from '../util/fetchWithRetries';
1014
import { getRandomItem } from '../util/getRandomItem';
11-
12-
export class GeminiKeysNotSetError extends Error {
13-
constructor(message: string) {
14-
super(message);
15-
this.name = 'GeminiKeysNotSetError';
16-
}
17-
}
18-
19-
export class GeminiAPIError extends Error {
20-
constructor(public status: number, message: string) {
21-
super(message);
22-
this.name = 'GeminiAPIError';
23-
}
24-
}
15+
import { GeminiKeysNotSetError } from '../exceptions/GeminiKeyNotSetError';
16+
import { GeminiAPIError } from '../exceptions/GeminiAPIError';
2517

2618
export class GeminiService implements GenerationService {
2719
private readonly settingsService: SettingsService;
28-
private genAI?: GoogleGenerativeAI;
2920

3021
constructor(settingsService: SettingsService) {
3122
this.settingsService = settingsService;
@@ -40,11 +31,18 @@ export class GeminiService implements GenerationService {
4031
}
4132

4233
getGeminiKeys(): string[] {
43-
const keysString = this.settingsService.get(SettingsEnum.GEMINI_KEYS) as string;
34+
const keysString = this.settingsService.get(
35+
SettingsEnum.GEMINI_KEYS
36+
) as string;
4437
if (!keysString || keysString.trim() === '') {
45-
throw new GeminiKeysNotSetError('No Gemini API keys set. Please configure them in settings (e.g., key1,key2,key3).');
38+
throw new GeminiKeysNotSetError(
39+
'No Gemini API keys set. Please configure them in settings (e.g., key1,key2,key3).'
40+
);
4641
}
47-
return keysString.split(',').map(key => key.trim()).filter(key => key.length > 0);
42+
return keysString
43+
.split(',')
44+
.map((key) => key.trim())
45+
.filter((key) => key.length > 0);
4846
}
4947

5048
private getGenAIClient(): GoogleGenerativeAI {
@@ -54,23 +52,39 @@ export class GeminiService implements GenerationService {
5452
}
5553

5654
private readonly safetySettings = [
57-
{ category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
58-
{ category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE },
59-
{ category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_NONE },
60-
{ category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_NONE },
55+
{
56+
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
57+
threshold: HarmBlockThreshold.BLOCK_NONE,
58+
},
59+
{
60+
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
61+
threshold: HarmBlockThreshold.BLOCK_NONE,
62+
},
63+
{
64+
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
65+
threshold: HarmBlockThreshold.BLOCK_NONE,
66+
},
67+
{
68+
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
69+
threshold: HarmBlockThreshold.BLOCK_NONE,
70+
},
6171
];
6272

63-
async sentientSimsGenerate(request: OpenAICompatibleRequest, retries: number = 3): Promise<SimsGenerateResponse> {
73+
async sentientSimsGenerate(
74+
request: OpenAICompatibleRequest,
75+
retries: number = 3
76+
): Promise<SimsGenerateResponse> {
6477
const genAI = this.getGenAIClient();
6578
const model = genAI.getGenerativeModel({
6679
model: this.getGeminiModel(),
6780
safetySettings: this.safetySettings,
68-
systemInstruction: request.messages.find(msg => msg.role === 'system')?.content,
81+
systemInstruction: request.messages.find((msg) => msg.role === 'system')
82+
?.content,
6983
});
7084

7185
const contents = request.messages
72-
.filter(msg => msg.role !== 'system')
73-
.map(msg => ({
86+
.filter((msg) => msg.role !== 'system')
87+
.map((msg) => ({
7488
role: msg.role === 'assistant' ? 'model' : 'user',
7589
parts: [{ text: msg.content }],
7690
}));
@@ -87,54 +101,76 @@ export class GeminiService implements GenerationService {
87101
};
88102
log.debug(`Full Gemini Request: ${JSON.stringify(fullRequest, null, 2)}`);
89103

90-
let text: string;
91-
for (let attempt = 1; attempt <= retries; attempt++) {
104+
let text: string | undefined;
105+
for (let attempt = 0; attempt < retries; attempt += 1) {
92106
try {
107+
// eslint-disable-next-line no-await-in-loop
93108
const result = await model.generateContent(fullRequest);
94109
text = result.response.text();
95110
log.debug(`Gemini Response: ${text}`);
96111
break;
97112
} catch (error: any) {
98-
const status = error.status || (error.response?.status);
99113
const message = error.message || 'Unknown error';
100-
log.error(`Gemini Error on attempt ${attempt}/${retries}: ${message}`, error);
114+
log.error(
115+
`Gemini Error on attempt ${attempt}/${retries}: ${message}`,
116+
error
117+
);
101118
if (attempt === retries) throw error;
102119
}
103120
}
104121

105122
if (this.settingsService.get(SettingsEnum.LOCALIZATION_ENABLED) && text) {
106-
const language = this.settingsService.get(SettingsEnum.LOCALIZATION_LANGUAGE);
123+
const language = this.settingsService.get(
124+
SettingsEnum.LOCALIZATION_LANGUAGE
125+
);
107126
if (language) {
108-
const translationGenAI = this.getGenAIClient(); // Случайный ключ для перевода
127+
const translationGenAI = this.getGenAIClient();
109128
const translationModel = translationGenAI.getGenerativeModel({
110-
model: this.getGeminiModel(), // Та же модель, что и для генерации
129+
model: this.getGeminiModel(),
111130
safetySettings: this.safetySettings,
112131
});
113132

114133
const translationRequest = {
115-
contents: [{
116-
role: 'user',
117-
parts: [{ text: `Do not include any additional text like "Here is your translation" or other explanations—just the response itself in ${language}. Translate this text to ${language}: ${text}` }],
118-
}],
134+
contents: [
135+
{
136+
role: 'user',
137+
parts: [
138+
{
139+
text: `Do not include any additional text like "Here is your translation" or other explanations—just the response itself in ${language}. Translate this text to ${language}: ${text}`,
140+
},
141+
],
142+
},
143+
],
119144
generationConfig: {
120145
maxOutputTokens: request.maxResponseTokens,
121146
temperature: 0.8,
122147
topP: 0.9,
123148
},
124149
safetySettings: this.safetySettings,
125150
};
126-
log.debug(`Gemini Translation Request: ${JSON.stringify(translationRequest, null, 2)}`);
127-
128-
for (let attempt = 1; attempt <= retries; attempt++) {
151+
log.debug(
152+
`Gemini Translation Request: ${JSON.stringify(
153+
translationRequest,
154+
null,
155+
2
156+
)}`
157+
);
158+
159+
for (let attempt = 0; attempt < retries; attempt += 1) {
129160
try {
130-
const translationResult = await translationModel.generateContent(translationRequest);
161+
// eslint-disable-next-line no-await-in-loop
162+
const translationResult = await translationModel.generateContent(
163+
translationRequest
164+
);
131165
text = translationResult.response.text();
132166
log.debug(`Gemini Translated Response: ${text}`);
133167
break;
134168
} catch (error: any) {
135-
const status = error.status || (error.response?.status);
136169
const message = error.message || 'Unknown error';
137-
log.error(`Gemini Translation Error on attempt ${attempt}/${retries}: ${message}`, error);
170+
log.error(
171+
`Gemini Translation Error on attempt ${attempt}/${retries}: ${message}`,
172+
error
173+
);
138174
if (attempt === retries) throw error;
139175
}
140176
}
@@ -183,7 +219,10 @@ export class GeminiService implements GenerationService {
183219

184220
if (!response.ok) {
185221
const errorText = await response.text();
186-
throw new GeminiAPIError(response.status, `Failed to fetch Gemini models: ${response.status} - ${errorText}`);
222+
throw new GeminiAPIError(
223+
response.status,
224+
`Failed to fetch Gemini models: ${response.status} - ${errorText}`
225+
);
187226
}
188227

189228
const data = await response.json();
@@ -200,7 +239,10 @@ export class GeminiService implements GenerationService {
200239
}
201240
return [
202241
{ name: 'gemini-1.5-flash', displayName: 'Gemini 1.5 Flash' },
203-
{ name: 'gemini-2.0-flash-exp', displayName: 'Gemini 2.0 Flash Experimental' },
242+
{
243+
name: 'gemini-2.0-flash-exp',
244+
displayName: 'Gemini 2.0 Flash Experimental',
245+
},
204246
];
205247
}
206248
}

0 commit comments

Comments
 (0)