Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": "0.2.0",
"configurations": [
"configurations": [
{
"type": "node",
"request": "launch",
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ English / [Japanese](./README_jp.md)
> The "Generator" setting will be moved to the Avatar Settings section, and the existing "Generator" will be renamed to "Model."
> We plan to clarify the relationship between Avatar AI, which is centered around context and is composed of a collection of multiple generators.

> As a temporary measure, we have added cutoffChatLimit to the LLM settings.
> This will reduce token consumption by forcibly truncating the context.

> Supported LM Studio API (OpenAI compatibility mode).

> Note: If you are using a previous version and are experiencing unstable operation, please initialize the settings by going to SystemSetting > initialize.
Expand Down
3 changes: 3 additions & 0 deletions README_jp.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Japanese / [English](./README.md)
> 「ジェネレーター」の設定はアバター設定側に移動し、既存の「ジェネレーター」は「モデル」に変更します。
> アバターAIがコンテキストを中心として複数のジェネレーターの集合体で構成される関係性をより明確化する予定です。

> 暫定的な対策としてLLM設定に cutoffChatLimit を追加しました。
> 強制的にコンテキストを切り詰めることでトークンの消費量を抑制します。

> LM Studio API (OpenAI互換モード)をサポートしました。

> Note: 以前の版を使っている方で、動作が不安定になった方はSystemSetting > initialize で一旦設定の初期化をお願いします。
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "avatar-shell",
"description": "Avatar/Visually-based MCP client",
"version": "0.2.5",
"version": "0.2.6",
"license": "Apache-2.0",
"private": true,
"type": "module",
Expand Down Expand Up @@ -84,5 +84,8 @@
"sound-play": "^1.1.0",
"ws": "^8.18.2",
"zod": "^3.25.34"
},
"volta": {
"node": "24.13.0"
}
}
4 changes: 3 additions & 1 deletion packages/common/DefGenerators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export const ContextGeneratorSettingSchema = Schema.partial(Schema.Struct({
toContext:Schema.optional(AsContextLinesSchema),
noTool: Schema.optional(Schema.Boolean),
useModel: Schema.optional(Schema.String),
cutoffChatLimit: Schema.optional(Schema.Number),
debug: Schema.Any, // デバッグ用汎用
}))

Expand Down Expand Up @@ -174,7 +175,8 @@ Generator 全体設定(api keyなど全体で設定するもの

const generatorCommonConfigSchema = Schema.Struct({
maxTokenThreshold:Schema.Number,
summarizePrompt:Schema.String
summarizePrompt:Schema.String,
cutoffChatLimit:Schema.optional(Schema.Number)
})

// openAi openAiText, openAiImage, openAiVoice
Expand Down
3 changes: 3 additions & 0 deletions packages/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,8 @@
"electron-devtools-installer": "4.0.0",
"typescript": "5.8.3",
"vite": "7.0.5"
},
"volta": {
"node": "24.13.0"
}
}
15 changes: 9 additions & 6 deletions packages/main/src/AvatarState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@ export class AvatarState {
});
}

MaxGen = 2; // TODO 世代の最適値は
MaxGen = 20; // TODO 世代の最適値は

enterInner(inner: GenInner, addToBuffer = true) {
const it = this;
Expand Down Expand Up @@ -893,6 +893,7 @@ export class AvatarState {
// }
yield* it.resetPrevBuffer();
it.clearStreamingText();
// gen.setContextDelimiter() ここは次に回すgenが未確定だからデリミダ付けられないか。。
return; // func の無限ループを防ぐ
}
// Generator処理
Expand Down Expand Up @@ -922,6 +923,7 @@ export class AvatarState {
yield* Queue.offerAll(it.outerQueue, io);
} else {
it.clearStreamingText();
gen.setContextDelimiter()
}
}),
step: b => loop,
Expand Down Expand Up @@ -1057,11 +1059,12 @@ export class AvatarState {
toGenerator: gen,
input: message,
genNum: 1, // この入力はまだcontextに追加されていないので1から開始して追加させる
setting: {
toClass: setting.toClass,
toRole: setting.toRole,
toContext: setting.toContext,
},
setting: setting,
// setting: {
// toClass: setting.toClass,
// toRole: setting.toRole,
// toContext: setting.toContext,
// },
}, addToBuffer);
// console.log('start loop');

Expand Down
41 changes: 35 additions & 6 deletions packages/main/src/generators/ClaudeGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ export abstract class ClaudeBaseGenerator extends ContextGenerator {
protected abstract genName: GeneratorProvider;
protected maxModelContextSize = 200000; // TODO Claudeの最大コンテキスト長も20Kぐらいらしい

protected get previousContexts() {
return this.previousNativeContexts as Anthropic.Messages.MessageParam[];
}
// protected get previousContexts() {
// return this.previousNativeContexts as Anthropic.Messages.MessageParam[];
// }

static generatorInfo: ContextGeneratorInfo = {
usePreviousContext: true,
Expand All @@ -58,6 +58,10 @@ export abstract class ClaudeBaseGenerator extends ContextGenerator {
});
}

getPreviousNativeContexts():(Anthropic.Messages.MessageParam | null)[] {
return this.previousNativeContexts as (Anthropic.Messages.MessageParam | null)[];
}

// filterToolRes(value: ContentBlock ) {
// // sysConfigでexperimental.mcpUiFilterDisabledがtrueの場合フィルタしない
// if(this.sysSetting.experimental.mcpUiFilterDisabled) return [value];
Expand Down Expand Up @@ -279,7 +283,32 @@ export class ClaudeTextGenerator extends ClaudeBaseGenerator {
return Effect.gen(function* () {
// TODO prev contextを抽出(AsMessage履歴から合成またはコンテキストキャッシュから再生)
const prevMake = yield* it.makePreviousContext(avatarState,current);
const prev = Array.from(it.previousContexts) // ユーザからのmcpExternalで
let prev: Anthropic.Messages.MessageParam[] = []
const lessCutoff = it.sysSetting.generators.anthropic.common?.cutoffChatLimit || Number.MAX_SAFE_INTEGER;
if (lessCutoff !== Number.MAX_SAFE_INTEGER) {
const sum = Array.from(it.getPreviousNativeContexts()).reduce((previousValue, currentValue) => {
if(currentValue === null){
previousValue.list.push(previousValue.buf)
return {list:previousValue.list,buf:[]}
}
return {list:previousValue.list,buf:previousValue.buf.concat(currentValue)}
},{list:[] as Anthropic.Messages.MessageParam[][],buf:[] as Anthropic.Messages.MessageParam[]})
if(sum.buf.length > 0) {
sum.list.push(sum.buf)
}

const cutoff = sum.list.reverse().reduce((previousValue, currentValue) => {
if (previousValue.count <= 0) {
return previousValue;
}
const next = previousValue.out.concat(currentValue.reverse());
previousValue.count-= currentValue.length
return {out:next,count:previousValue.count}
},{out:[] as Anthropic.Messages.MessageParam[][],count:lessCutoff})
prev = cutoff.out.reverse().flat()
} else {
prev = Array.from(it.getPreviousNativeContexts()).filter((value):value is Anthropic.Messages.MessageParam => value !== null); // ユーザからのmcpExternalで
}
// 入力current GenInnerからcurrent contextを調整(input textまはたMCP responses)
const mes = yield* it.makeCurrentContext(current);

Expand Down Expand Up @@ -317,11 +346,11 @@ export class ClaudeTextGenerator extends ClaudeBaseGenerator {
},
});

it.previousContexts.push({
it.getPreviousNativeContexts().push({
role: mes.role,
content:mes.content,
})
it.previousContexts.push({
it.getPreviousNativeContexts().push({
role: message.role,
content:message.content,
} as Anthropic.Messages.MessageParam)
Expand Down
8 changes: 8 additions & 0 deletions packages/main/src/generators/ContextGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ export abstract class ContextGenerator {
avatarState.clearStreamingText()
}

/**
* prevContextBufferに分離可能なデリミダを追加する
* 今のところnullを使う
*/
setContextDelimiter() {
this.previousNativeContexts.push(null);
}

filterForLlmPrevContext(asMes:AsMessage[],current?:AsMessage) {
// TODO ここは妥当な条件を再検討が必要。。
// currentを含まないこと、currentよりもtickが古いこと
Expand Down
32 changes: 29 additions & 3 deletions packages/main/src/generators/GeminiGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export abstract class GeminiBaseGenerator extends ContextGenerator {
}

protected get previousContexts() {
return this.previousNativeContexts as Content[];
return this.previousNativeContexts as (Content | null)[];
}

constructor(sysConfig: SysConfig, settings?: GeminiSettings) {
Expand Down Expand Up @@ -155,7 +155,7 @@ export abstract class GeminiBaseGenerator extends ContextGenerator {
// geminiの場合、画像ファイルはinput_imageとしてbase64で送る
const media = yield* DocService.readDocMedia(current.input.content.mediaUrl);
const b1 = yield* it.shrinkImage(Buffer.from(media, 'base64').buffer, it.geminiSettings?.inWidth);
const blob = new Blob([b1], {type: 'image/png'});
const blob = new Blob([new Uint8Array(b1).buffer], {type: 'image/png'});
const myfile = yield* Effect.tryPromise(() => it.ai.files.upload({
file: blob,
config: {mimeType: 'image/png'},
Expand Down Expand Up @@ -211,7 +211,33 @@ export class GeminiTextGenerator extends GeminiBaseGenerator {
return Effect.gen(function* () {
// prev contextを抽出(AsMessage履歴から合成またはコンテキストキャッシュから再生)
const prevMake = yield* it.makePreviousContext(avatarState,current);
const prev = Array.from(it.previousContexts)
let prev:Content[] = []
const lessCutoff = it.sysSetting.generators.gemini.common?.cutoffChatLimit || Number.MAX_SAFE_INTEGER;
if (lessCutoff !== Number.MAX_SAFE_INTEGER) {
const sum = Array.from(it.previousContexts).reduce((previousValue, currentValue) => {
if(currentValue === null){
previousValue.list.push(previousValue.buf)
return {list:previousValue.list,buf:[]}
}
return {list:previousValue.list,buf:previousValue.buf.concat(currentValue)}
},{list:[] as Content[][],buf:[] as Content[]})
if(sum.buf.length > 0) {
sum.list.push(sum.buf)
}

const cutoff = sum.list.reverse().reduce((previousValue, currentValue) => {
if (previousValue.count <= 0) {
return previousValue;
}
const next = previousValue.out.concat(currentValue.reverse());
previousValue.count-= currentValue.length
return {out:next,count:previousValue.count}
},{out:[] as Content[][],count:lessCutoff})
prev = cutoff.out.reverse().flat()
} else {
prev = Array.from(it.previousContexts).filter((value):value is Content => value !== null); // ユーザからのmcpExternalで
}
// const prev = Array.from(it.previousContexts)
// 入力current GenInnerからcurrent contextを調整(input textまはたMCP responses)
const mes = yield* it.makeCurrentContext(current);

Expand Down
38 changes: 35 additions & 3 deletions packages/main/src/generators/LmStudioGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export abstract class LmStudioBaseGenerator extends ContextGenerator {
protected override maxModelContextSize = -1;
protected baseUrl=''

getPreviousNativeContexts():(ResponseInputItem | null)[] {
return this.previousNativeContexts as (ResponseInputItem | null)[];
}

static generatorInfo: ContextGeneratorInfo = {
usePreviousContext: true,
defaultPrevContextSize: 100,
Expand Down Expand Up @@ -263,7 +267,35 @@ export class LmStudioTextGenerator extends LmStudioBaseGenerator {

// prev contextを抽出(AsMessage履歴から合成またはコンテキストキャッシュから再生)
const prevMake = yield* it.makePreviousContext(avatarState, current);
const prev = Array.from(it.previousNativeContexts).filter(value => !(value.type === 'message' && value.role === 'developer'))
let prev:ResponseInputItem[] = []
const lessCutoff = it.sysSetting.generators.lmStudio.common?.cutoffChatLimit || Number.MAX_SAFE_INTEGER;
// const lessCutoff = Math.min(current.setting?.cutoffChatLimit || Number.MAX_SAFE_INTEGER, it.lmStudioSettings?.cutoffChatLimit || Number.MAX_SAFE_INTEGER);
if (lessCutoff !== Number.MAX_SAFE_INTEGER) {
const sum = Array.from(it.getPreviousNativeContexts()).reduce((previousValue, currentValue) => {
if(currentValue === null){
previousValue.list.push(previousValue.buf)
return {list:previousValue.list,buf:[]}
}
return {list:previousValue.list,buf:previousValue.buf.concat(currentValue)}
},{list:[] as ResponseInputItem[][],buf:[] as ResponseInputItem[]})
if(sum.buf.length > 0) {
sum.list.push(sum.buf)
}

const cutoff = sum.list.reverse().reduce((previousValue, currentValue) => {
if (previousValue.count <= 0) {
return previousValue;
}
const next = previousValue.out.concat(currentValue.reverse());
previousValue.count-= currentValue.length
return {out:next,count:previousValue.count}
},{out:[] as ResponseInputItem[][],count:lessCutoff})
prev = cutoff.out.reverse().flat().filter(value => !(value.type === 'message' && value.role === 'developer'))
} else {
prev = Array.from(it.getPreviousNativeContexts()).filter((value):value is ResponseInputItem => value !== null && !(value.type === 'message' && value.role === 'developer'))
// prev = Array.from(it.getPreviousNativeContexts()).filter((value):value is Anthropic.Messages.MessageParam => value !== null); // ユーザからのmcpExternalで
}
// const prev = Array.from(it.getPreviousNativeContexts()).filter((value):value is ResponseInputItem => value !== null && !(value.type === 'message' && value.role === 'developer'))
// TODO prevMakeとprevの差分チェックは後々必要
// 入力current GenInnerからcurrent contextを調整(input textまはたMCP responses)
const mes = yield* it.makeCurrentContext(current);
Expand All @@ -290,7 +322,7 @@ export class LmStudioTextGenerator extends LmStudioBaseGenerator {
// store:true,
};
mes.forEach(a => {
it.previousNativeContexts.push(a)
it.getPreviousNativeContexts().push(a)
})

const res = yield* Effect.tryPromise({
Expand Down Expand Up @@ -328,7 +360,7 @@ export class LmStudioTextGenerator extends LmStudioBaseGenerator {
).map(a => a.response.output).flat();
console.log('responseOut:', JSON.stringify(responseOut));
responseOut.forEach(a => {
it.previousNativeContexts.push(a) // TODO ResponseOutputItemをResponseInputItemに変換する必要があるケースがあったはず。。
it.getPreviousNativeContexts().push(a) // TODO ResponseOutputItemをResponseInputItemに変換する必要があるケースがあったはず。。
})
const textOut = responseOut.filter(b => b.type === 'message').map((b: ResponseOutputMessage) => {
// TODO mesIdは1件のはず?
Expand Down
32 changes: 30 additions & 2 deletions packages/main/src/generators/OllamaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ export class OllamaTextGenerator extends ContextGenerator {
protected model = 'llama3.2';
private ollama: Ollama;
protected systemPrompt?:Message[];
protected ollamaSettings: ContextGeneratorSetting | undefined;
// TODO ollamaの場合最大コンテキストサイズの取得は?

protected get previousContexts() {
return this.previousNativeContexts as Message[];
return this.previousNativeContexts as (Message | null)[];
}

static make(sysConfig:SysConfig, settings?: ContextGeneratorSetting) {
Expand All @@ -37,6 +38,7 @@ export class OllamaTextGenerator extends ContextGenerator {

constructor(setting: SysConfig,settings?: ContextGeneratorSetting) {
super(setting);
this.ollamaSettings = settings;
this.model = settings?.useModel || setting.generators.ollama?.model || 'llama3.1';
this.ollama = new Ollama({
host: setting.generators.ollama?.host || 'http://localhost:11434',
Expand Down Expand Up @@ -64,7 +66,33 @@ export class OllamaTextGenerator extends ContextGenerator {
images:undefined, // TODO 画像は送るべきか?
} as Message]
})
const prev = Array.from(it.previousContexts)
let prev:Message[] = []
const lessCutoff = it.sysSetting.generators.ollama.common?.cutoffChatLimit || Number.MAX_SAFE_INTEGER;
if (lessCutoff !== Number.MAX_SAFE_INTEGER) {
const sum = Array.from(it.previousContexts).reduce((previousValue, currentValue) => {
if(currentValue === null){
previousValue.list.push(previousValue.buf)
return {list:previousValue.list,buf:[]}
}
return {list:previousValue.list,buf:previousValue.buf.concat(currentValue)}
},{list:[] as Message[][],buf:[] as Message[]})
if(sum.buf.length > 0) {
sum.list.push(sum.buf)
}
const cutoff = sum.list.reverse().reduce((previousValue, currentValue) => {
if (previousValue.count <= 0) {
return previousValue;
}
const next = previousValue.out.concat(currentValue.reverse());
previousValue.count-= currentValue.length
return {out:next,count:previousValue.count}
},{out:[] as Message[][],count:lessCutoff})
prev = cutoff.out.reverse().flat()
} else {
prev = Array.from(it.previousContexts).filter((value):value is Message => value !== null); // ユーザからのmcpExternalで
}

// const prev = Array.from(it.previousContexts)
// 入力current GenInnerからcurrent contextを調整(input textまはたMCP responses)
const mes:Message = { role: 'user', content: '' };
if (current.input?.content.text) {
Expand Down
Loading
Loading