diff --git a/bun.lock b/bun.lock index f82aa8f..06a22a6 100644 --- a/bun.lock +++ b/bun.lock @@ -108,6 +108,8 @@ "itty-router": "^5.0.18", "mobx": "^6.13.5", "mobx-state-tree": "^6.0.1", + "vite": "^6.3.5", + "vitest": "^3.1.4", }, }, "packages/schema": { diff --git a/packages/ai/package.json b/packages/ai/package.json index b964b17..4885bc3 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -2,6 +2,36 @@ "name": "@open-gsio/ai", "type": "module", "module": "src/index.ts", + "exports": { + ".": { + "import": "./src/index.ts", + "types": "./src/index.ts" + }, + "./chat-sdk/chat-sdk.ts": { + "import": "./src/chat-sdk/chat-sdk.ts", + "types": "./src/chat-sdk/chat-sdk.ts" + }, + "./providers/_ProviderRepository.ts": { + "import": "./src/providers/_ProviderRepository.ts", + "types": "./src/providers/_ProviderRepository.ts" + }, + "./providers/google.ts": { + "import": "./src/providers/google.ts", + "types": "./src/providers/google.ts" + }, + "./providers/openai.ts": { + "import": "./src/providers/openai.ts", + "types": "./src/providers/openai.ts" + }, + "./src": { + "import": "./src/index.ts", + "types": "./src/index.ts" + }, + "./utils": { + "import": "./src/utils/index.ts", + "types": "./src/utils/index.ts" + } + }, "scripts": { "tests": "vitest run", "tests:coverage": "vitest run --coverage.enabled=true" diff --git a/packages/ai/src/__tests__/assistant-sdk.test.ts b/packages/ai/src/__tests__/assistant-sdk.test.ts index d98f914..68c103d 100644 --- a/packages/ai/src/__tests__/assistant-sdk.test.ts +++ b/packages/ai/src/__tests__/assistant-sdk.test.ts @@ -4,7 +4,7 @@ import { AssistantSdk } from '../assistant-sdk'; import { Utils } from '../utils/utils.ts'; // Mock dependencies -vi.mock('../utils', () => ({ +vi.mock('../utils/utils.ts', () => ({ Utils: { selectEquitably: vi.fn(), getCurrentDate: vi.fn(), @@ -26,8 +26,7 @@ describe('AssistantSdk', () => { vi.setSystemTime(new Date('2023-01-01T12:30:45Z')); // Reset mocks - vi.mocked(Utils.selectEquitably).mockReset(); - vi.mocked(Utils.getCurrentDate).mockReset(); + vi.resetAllMocks(); }); afterEach(() => { @@ -37,11 +36,11 @@ describe('AssistantSdk', () => { describe('getAssistantPrompt', () => { it('should return a prompt with default values when minimal params are provided', () => { // Mock dependencies - vi.mocked(Utils.selectEquitably).mockReturnValue({ + Utils.selectEquitably.mockReturnValue({ question1: 'answer1', question2: 'answer2', }); - vi.mocked(Utils.getCurrentDate).mockReturnValue('2023-01-01T12:30:45Z'); + Utils.getCurrentDate.mockReturnValue('2023-01-01T12:30:45Z'); const prompt = AssistantSdk.getAssistantPrompt({}); @@ -54,11 +53,11 @@ describe('AssistantSdk', () => { it('should include maxTokens when provided', () => { // Mock dependencies - vi.mocked(Utils.selectEquitably).mockReturnValue({ + Utils.selectEquitably.mockReturnValue({ question1: 'answer1', question2: 'answer2', }); - vi.mocked(Utils.getCurrentDate).mockReturnValue('2023-01-01T12:30:45Z'); + Utils.getCurrentDate.mockReturnValue('2023-01-01T12:30:45Z'); const prompt = AssistantSdk.getAssistantPrompt({ maxTokens: 1000 }); @@ -67,11 +66,11 @@ describe('AssistantSdk', () => { it('should use provided userTimezone and userLocation', () => { // Mock dependencies - vi.mocked(Utils.selectEquitably).mockReturnValue({ + Utils.selectEquitably.mockReturnValue({ question1: 'answer1', question2: 'answer2', }); - vi.mocked(Utils.getCurrentDate).mockReturnValue('2023-01-01T12:30:45Z'); + Utils.getCurrentDate.mockReturnValue('2023-01-01T12:30:45Z'); const prompt = AssistantSdk.getAssistantPrompt({ userTimezone: 'America/New_York', @@ -84,12 +83,12 @@ describe('AssistantSdk', () => { it('should use current date when Utils.getCurrentDate is not available', () => { // Mock dependencies - vi.mocked(Utils.selectEquitably).mockReturnValue({ + Utils.selectEquitably.mockReturnValue({ question1: 'answer1', question2: 'answer2', }); // @ts-expect-error - is supposed to break - vi.mocked(Utils.getCurrentDate).mockReturnValue(undefined); + Utils.getCurrentDate.mockReturnValue(undefined); const prompt = AssistantSdk.getAssistantPrompt({}); @@ -99,8 +98,8 @@ describe('AssistantSdk', () => { it('should use few_shots directly when Utils.selectEquitably is not available', () => { // @ts-expect-error - is supposed to break - vi.mocked(Utils.selectEquitably).mockReturnValue(undefined); - vi.mocked(Utils.getCurrentDate).mockReturnValue('2023-01-01T12:30:45Z'); + Utils.selectEquitably.mockReturnValue(undefined); + Utils.getCurrentDate.mockReturnValue('2023-01-01T12:30:45Z'); const prompt = AssistantSdk.getAssistantPrompt({}); diff --git a/packages/ai/src/__tests__/chat-sdk.test.ts b/packages/ai/src/__tests__/chat-sdk.test.ts index 70f7043..320ea69 100644 --- a/packages/ai/src/__tests__/chat-sdk.test.ts +++ b/packages/ai/src/__tests__/chat-sdk.test.ts @@ -1,8 +1,9 @@ -import { Message } from '@open-gsio/schema'; +import { Schema } from '@open-gsio/schema'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { AssistantSdk } from '../assistant-sdk'; import { ChatSdk } from '../chat-sdk'; +import { ProviderRepository } from '../providers/_ProviderRepository.ts'; // Mock dependencies vi.mock('../assistant-sdk', () => ({ @@ -11,15 +12,17 @@ vi.mock('../assistant-sdk', () => ({ }, })); -vi.mock('../../models/Message', () => ({ - default: { - create: vi.fn(message => message), +vi.mock('@open-gsio/schema', () => ({ + Schema: { + Message: { + create: vi.fn(message => message), + }, }, })); -vi.mock('../../providers/_ProviderRepository', () => ({ +vi.mock('../providers/_ProviderRepository', () => ({ ProviderRepository: { - getModelFamily: vi.fn(), + getModelFamily: vi.fn().mockResolvedValue('openai'), }, })); @@ -35,7 +38,7 @@ describe('ChatSdk', () => { const result = await ChatSdk.preprocess({ messages }); - expect(Message.create).toHaveBeenCalledWith({ + expect(Schema.Message.create).toHaveBeenCalledWith({ role: 'assistant', content: '', }); @@ -155,80 +158,80 @@ describe('ChatSdk', () => { describe('buildMessageChain', () => { // TODO: Fix this test - // it('should build a message chain with system role for most models', async () => { - // vi.mocked(ProviderRepository.getModelFamily).mockResolvedValue('openai'); - // - // const messages = [{ role: 'user', content: 'Hello' }]; - // - // const opts = { - // systemPrompt: 'System prompt', - // assistantPrompt: 'Assistant prompt', - // toolResults: { role: 'tool', content: 'Tool result' }, - // model: 'gpt-4', - // }; - // - // const result = await ChatSdk.buildMessageChain(messages, opts as any); - // - // expect(ProviderRepository.getModelFamily).toHaveBeenCalledWith('gpt-4', undefined); - // expect(Message.create).toHaveBeenCalledTimes(3); - // expect(Message.create).toHaveBeenNthCalledWith(1, { - // role: 'system', - // content: 'System prompt', - // }); - // expect(Message.create).toHaveBeenNthCalledWith(2, { - // role: 'assistant', - // content: 'Assistant prompt', - // }); - // expect(Message.create).toHaveBeenNthCalledWith(3, { - // role: 'user', - // content: 'Hello', - // }); - // }); - // TODO: Fix this test - // it('should build a message chain with assistant role for o1, gemma, claude, or google models', async () => { - // vi.mocked(ProviderRepository.getModelFamily).mockResolvedValue('claude'); - // - // const messages = [{ role: 'user', content: 'Hello' }]; - // - // const opts = { - // systemPrompt: 'System prompt', - // assistantPrompt: 'Assistant prompt', - // toolResults: { role: 'tool', content: 'Tool result' }, - // model: 'claude-3', - // }; - // - // const result = await ChatSdk.buildMessageChain(messages, opts as any); - // - // expect(ProviderRepository.getModelFamily).toHaveBeenCalledWith('claude-3', undefined); - // expect(Message.create).toHaveBeenCalledTimes(3); - // expect(Message.create).toHaveBeenNthCalledWith(1, { - // role: 'assistant', - // content: 'System prompt', - // }); - // }); - // TODO: Fix this test - // it('should filter out messages with empty content', async () => { - // // - // vi.mocked(ProviderRepository.getModelFamily).mockResolvedValue('openai'); - // - // const messages = [ - // { role: 'user', content: 'Hello' }, - // { role: 'user', content: '' }, - // { role: 'user', content: ' ' }, - // { role: 'user', content: 'World' }, - // ]; - // - // const opts = { - // systemPrompt: 'System prompt', - // assistantPrompt: 'Assistant prompt', - // toolResults: { role: 'tool', content: 'Tool result' }, - // model: 'gpt-4', - // }; - // - // const result = await ChatSdk.buildMessageChain(messages, opts as any); - // - // // 2 system/assistant messages + 2 user messages (Hello and World) - // expect(Message.create).toHaveBeenCalledTimes(4); - // }); + it('should build a message chain with system role for most models', async () => { + ProviderRepository.getModelFamily.mockResolvedValue('openai'); + + const messages = [{ role: 'user', content: 'Hello' }]; + + const opts = { + systemPrompt: 'System prompt', + assistantPrompt: 'Assistant prompt', + toolResults: { role: 'tool', content: 'Tool result' }, + model: 'gpt-4', + env: {}, + }; + + const result = await ChatSdk.buildMessageChain(messages, opts as any); + + expect(ProviderRepository.getModelFamily).toHaveBeenCalledWith('gpt-4', {}); + expect(Schema.Message.create).toHaveBeenCalledTimes(3); + expect(Schema.Message.create).toHaveBeenNthCalledWith(1, { + role: 'system', + content: 'System prompt', + }); + expect(Schema.Message.create).toHaveBeenNthCalledWith(2, { + role: 'assistant', + content: 'Assistant prompt', + }); + expect(Schema.Message.create).toHaveBeenNthCalledWith(3, { + role: 'user', + content: 'Hello', + }); + }); + it('should build a message chain with assistant role for o1, gemma, claude, or google models', async () => { + ProviderRepository.getModelFamily.mockResolvedValue('claude'); + + const messages = [{ role: 'user', content: 'Hello' }]; + + const opts = { + systemPrompt: 'System prompt', + assistantPrompt: 'Assistant prompt', + toolResults: { role: 'tool', content: 'Tool result' }, + model: 'claude-3', + env: {}, + }; + + const result = await ChatSdk.buildMessageChain(messages, opts as any); + + expect(ProviderRepository.getModelFamily).toHaveBeenCalledWith('claude-3', {}); + expect(Schema.Message.create).toHaveBeenCalledTimes(3); + expect(Schema.Message.create).toHaveBeenNthCalledWith(1, { + role: 'assistant', + content: 'System prompt', + }); + }); + it('should filter out messages with empty content', async () => { + ProviderRepository.getModelFamily.mockResolvedValue('openai'); + + const messages = [ + { role: 'user', content: 'Hello' }, + { role: 'user', content: '' }, + { role: 'user', content: ' ' }, + { role: 'user', content: 'World' }, + ]; + + const opts = { + systemPrompt: 'System prompt', + assistantPrompt: 'Assistant prompt', + toolResults: { role: 'tool', content: 'Tool result' }, + model: 'gpt-4', + env: {}, + }; + + const result = await ChatSdk.buildMessageChain(messages, opts as any); + + // 2 system/assistant messages + 2 user messages (Hello and World) + expect(Schema.Message.create).toHaveBeenCalledTimes(4); + }); }); }); diff --git a/packages/ai/src/chat-sdk/chat-sdk.ts b/packages/ai/src/chat-sdk/chat-sdk.ts index c938f57..52fc1df 100644 --- a/packages/ai/src/chat-sdk/chat-sdk.ts +++ b/packages/ai/src/chat-sdk/chat-sdk.ts @@ -1,4 +1,4 @@ -import { Message } from '@open-gsio/schema'; +import { Schema } from '@open-gsio/schema'; import type { Instance } from 'mobx-state-tree'; import { OpenAI } from 'openai'; @@ -14,7 +14,7 @@ import type { export class ChatSdk { static async preprocess(params: PreprocessParams) { // a slot for to provide additional context - return Message.create({ + return Schema.Message.create({ role: 'assistant', content: '', }); @@ -105,7 +105,7 @@ export class ChatSdk { const messagesToSend = []; messagesToSend.push( - Message.create({ + Schema.Message.create({ role: opts.model.includes('o1') || opts.model.includes('gemma') || @@ -118,7 +118,7 @@ export class ChatSdk { ); messagesToSend.push( - Message.create({ + Schema.Message.create({ role: 'assistant', content: opts.assistantPrompt.trim(), }), @@ -127,7 +127,7 @@ export class ChatSdk { messagesToSend.push( ...messages .filter((message: any) => message.content?.trim()) - .map((message: any) => Message.create(message)), + .map((message: any) => Schema.Message.create(message)), ); return messagesToSend; diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 254ec8d..038bb31 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -1 +1,2 @@ export * from './providers'; +export * from './chat-sdk'; diff --git a/packages/router/package.json b/packages/router/package.json index d7dd5e2..66631f7 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -10,6 +10,8 @@ "@open-gsio/services": "workspace:*", "itty-router": "^5.0.18", "mobx": "^6.13.5", - "mobx-state-tree": "^6.0.1" + "mobx-state-tree": "^6.0.1", + "vitest": "^3.1.4", + "vite": "^6.3.5" } } diff --git a/packages/services/package.json b/packages/services/package.json index 88c70fa..6065416 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -2,6 +2,13 @@ "name": "@open-gsio/services", "type": "module", "module": "src/index.ts", + "version": "1.0.0", + "exports": { + ".": { + "import": "./src/index.ts", + "types": "./src/index.ts" + } + }, "scripts": { "tests": "vitest run", "tests:coverage": "vitest run --coverage.enabled=true" diff --git a/packages/services/src/__tests__/ChatService.test.ts b/packages/services/src/__tests__/ChatService.test.ts index 95491b6..a3d13e1 100644 --- a/packages/services/src/__tests__/ChatService.test.ts +++ b/packages/services/src/__tests__/ChatService.test.ts @@ -1,6 +1,5 @@ import { getSnapshot } from 'mobx-state-tree'; import OpenAI from 'openai'; -import { ChatSdk } from 'packages/ai/src/chat-sdk'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import ChatService, { ClientError } from '../chat-service/ChatService.ts'; diff --git a/packages/services/src/chat-service/ChatService.ts b/packages/services/src/chat-service/ChatService.ts index 889a979..8254850 100644 --- a/packages/services/src/chat-service/ChatService.ts +++ b/packages/services/src/chat-service/ChatService.ts @@ -13,10 +13,10 @@ import { OllamaChatSdk, XaiChatSdk, } from '@open-gsio/ai/src'; +import { Common } from '@open-gsio/ai/utils'; import { Schema } from '@open-gsio/schema'; import { flow, getSnapshot, types } from 'mobx-state-tree'; import OpenAI from 'openai'; -import { Common } from 'packages/ai/src/utils'; export interface StreamParams { env: Env;