Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vercel AI SDKを導入して各AIサービスの呼び出し処理を統一化 #150

Merged
merged 8 commits into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
1,367 changes: 1,346 additions & 21 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
"desktop": "NEXT_PUBLIC_BACKGROUND_IMAGE_PATH=\"\" run-p dev electron"
},
"dependencies": {
"@ai-sdk/anthropic": "^0.0.48",
"@ai-sdk/google": "^0.0.46",
"@ai-sdk/openai": "^0.0.54",
"@anthropic-ai/sdk": "^0.20.8",
"@charcoal-ui/icons": "^2.6.0",
"@google-cloud/text-to-speech": "^5.0.1",
Expand All @@ -26,18 +29,21 @@
"@pixiv/three-vrm": "^3.0.0",
"@tailwindcss/line-clamp": "^0.4.4",
"@vercel/analytics": "^1.3.1",
"ai": "^3.3.20",
"axios": "^1.6.8",
"canvas": "^2.11.2",
"formidable": "^3.5.1",
"groq-sdk": "^0.3.3",
"i18next": "^23.6.0",
"next": "^14.2.5",
"ollama-ai-provider": "^0.13.0",
"openai": "^4.38.5",
"pdfjs-dist": "^4.5.136",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^13.3.1",
"three": "^0.167.1",
"zod": "^3.23.8",
"zustand": "^4.5.4"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/components/chatLog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const ChatLog = () => {
/>
<ChatImage
role={msg.role}
imageUrl={msg.content[1].image_url.url}
imageUrl={msg.content[1].image}
characterName={characterName}
/>
</>
Expand Down
2 changes: 1 addition & 1 deletion src/components/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export const Menu = () => {
/>
)}
</div>
{selectAIService === 'openai' && !youtubeMode && (
{!youtubeMode && (
<>
<div className="order-3">
<IconButton
Expand Down
2 changes: 1 addition & 1 deletion src/components/settings/log.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const Log = () => {
></input>
) : (
<Image
src={value.content[1].image_url.url}
src={value.content[1].image}
alt="画像"
width={500}
height={500}
Expand Down
32 changes: 13 additions & 19 deletions src/components/settings/modelProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { SYSTEM_PROMPT } from '@/features/constants/systemPromptConstants'
import { Link } from '../link'
import { TextButton } from '../textButton'
import { useCallback } from 'react'
import { multiModalAIServices } from '@/features/stores/settings'

const ModelProvider = () => {
const webSocketMode = settingsStore((s) => s.webSocketMode)

const openAiKey = settingsStore((s) => s.openAiKey)
const openaiKey = settingsStore((s) => s.openaiKey)
const anthropicKey = settingsStore((s) => s.anthropicKey)
const googleKey = settingsStore((s) => s.googleKey)
const groqKey = settingsStore((s) => s.groqKey)
Expand All @@ -30,7 +31,7 @@ const ModelProvider = () => {
// ローカルLLMが選択された場合、AIモデルを空文字に設定
const defaultModels = {
openai: 'gpt-4o',
anthropic: 'claude-3.5-sonnet-20240620',
anthropic: 'claude-3-5-sonnet-20240620',
google: 'gemini-1.5-pro',
groq: 'gemma-7b-it',
localLlm: '',
Expand All @@ -44,24 +45,17 @@ const ModelProvider = () => {
selectAIModel: defaultModels[newService],
})

if (newService !== 'openai') {
if (!multiModalAIServices.includes(newService as any)) {
homeStore.setState({ modalImage: '' })
menuStore.setState({ showWebcam: false })

if (newService !== 'anthropic') {
settingsStore.setState({
conversationContinuityMode: false,
})
}

if (newService !== 'anthropic' && newService !== 'google') {
settingsStore.setState({
slideMode: false,
})
slideStore.setState({
isPlaying: false,
})
}
settingsStore.setState({
conversationContinuityMode: false,
slideMode: false,
})
slideStore.setState({
isPlaying: false,
})
}
},
[]
Expand Down Expand Up @@ -100,9 +94,9 @@ const ModelProvider = () => {
className="text-ellipsis px-16 py-8 w-col-span-2 bg-surface1 hover:bg-surface1-hover rounded-8"
type="text"
placeholder="sk-..."
value={openAiKey}
value={openaiKey}
onChange={(e) =>
settingsStore.setState({ openAiKey: e.target.value })
settingsStore.setState({ openaiKey: e.target.value })
}
/>
<div className="my-16">
Expand Down
17 changes: 10 additions & 7 deletions src/components/settings/slide.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useTranslation } from 'react-i18next'
import { useEffect, useState } from 'react'
import settingsStore from '@/features/stores/settings'
import settingsStore, {
multiModalAIServices,
multiModalAIServiceKey,
} from '@/features/stores/settings'
import menuStore from '@/features/stores/menu'
import slideStore from '@/features/stores/slide'
import { TextButton } from '../textButton'
Expand Down Expand Up @@ -66,9 +69,9 @@ const Slide = () => {
<TextButton
onClick={toggleSlideMode}
disabled={
selectAIService !== 'openai' &&
selectAIService !== 'anthropic' &&
selectAIService !== 'google'
!multiModalAIServices.includes(
selectAIService as multiModalAIServiceKey
)
}
>
{slideMode ? t('StatusOn') : t('StatusOff')}
Expand All @@ -92,9 +95,9 @@ const Slide = () => {
</option>
))}
</select>
{selectAIService === 'openai' && (
<SlideConvert onFolderUpdate={handleFolderUpdate} />
)}
{multiModalAIServices.includes(
selectAIService as multiModalAIServiceKey
) && <SlideConvert onFolderUpdate={handleFolderUpdate} />}
</>
)}
</>
Expand Down
68 changes: 61 additions & 7 deletions src/components/settings/slideConvert.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import settingsStore from '@/features/stores/settings'
import settingsStore, {
multiModalAIServiceKey,
multiModalAIServices,
} from '@/features/stores/settings'
import { TextButton } from '../textButton'

interface SlideConvertProps {
Expand All @@ -11,7 +14,9 @@ const SlideConvert: React.FC<SlideConvertProps> = ({ onFolderUpdate }) => {
const { t } = useTranslation()
const [file, setFile] = useState<File | null>(null)
const [folderName, setFolderName] = useState<string>('')
const [apiKey] = useState<string>(settingsStore.getState().openAiKey)
const aiService = settingsStore.getState()
.selectAIService as multiModalAIServiceKey

const [model, setModel] = useState<string>('gpt-4o')
const [isLoading, setIsLoading] = useState<boolean>(false)
const selectLanguage = settingsStore.getState().selectLanguage
Expand All @@ -27,6 +32,15 @@ const SlideConvert: React.FC<SlideConvertProps> = ({ onFolderUpdate }) => {

const handleFormSubmit = async (event: React.FormEvent) => {
event.preventDefault()

if (!multiModalAIServices.includes(aiService)) {
alert(t('InvalidAIService'))
return
}

const apiKeyName = `${aiService}Key` as const
const apiKey = settingsStore.getState()[apiKeyName]

if (!file || !folderName || !apiKey || !model) {
alert(t('PdfConvertSubmitError'))
return
Expand All @@ -37,6 +51,7 @@ const SlideConvert: React.FC<SlideConvertProps> = ({ onFolderUpdate }) => {
const formData = new FormData()
formData.append('file', file)
formData.append('folderName', folderName)
formData.append('aiService', aiService)
formData.append('apiKey', apiKey)
formData.append('model', model)
formData.append('selectLanguage', selectLanguage)
Expand Down Expand Up @@ -101,11 +116,50 @@ const SlideConvert: React.FC<SlideConvertProps> = ({ onFolderUpdate }) => {
onChange={(e) => setModel(e.target.value)}
className="text-ellipsis px-16 py-8 w-col-span-4 bg-surface1 hover:bg-surface1-hover rounded-8"
>
<option value="gpt-4o-mini">gpt-4o-mini</option>
<option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
<option value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
<option value="gpt-4o">gpt-4o(2024-05-13)</option>
<option value="gpt-4-turbo">gpt-4-turbo</option>
{aiService === 'openai' && (
<>
<option value="gpt-4o-mini">gpt-4o-mini</option>
<option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
<option value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
<option value="gpt-4o">gpt-4o(2024-05-13)</option>
<option value="gpt-4-turbo">gpt-4-turbo</option>
</>
)}
{aiService === 'anthropic' && (
<>
<option value="claude-3-opus-20240229">
claude-3-opus-20240229
</option>
<option value="claude-3-5-sonnet-20240620">
claude-3.5-sonnet-20240620
</option>
<option value="claude-3-sonnet-20240229">
claude-3-sonnet-20240229
</option>
<option value="claude-3-haiku-20240307">
claude-3-haiku-20240307
</option>
</>
)}
{aiService === 'google' && (
<>
<option value="gemini-1.5-flash-exp-0827">
gemini-1.5-flash-exp-0827
</option>
<option value="gemini-1.5-pro-exp-0827">
gemini-1.5-pro-exp-0827
</option>
<option value="gemini-1.5-flash-8b-exp-0827">
gemini-1.5-flash-8b-exp-0827
</option>
<option value="gemini-1.5-pro-latest">
gemini-1.5-pro-latest
</option>
<option value="gemini-1.5-flash-latest">
gemini-1.5-flash-latest
</option>
</>
)}
</select>
<div className="my-16">
<TextButton type="submit" disabled={isLoading}>
Expand Down
4 changes: 2 additions & 2 deletions src/components/settings/youtube.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import homeStore from '@/features/stores/home'
import menuStore from '@/features/stores/menu'
import settingsStore from '@/features/stores/settings'
import { TextButton } from '../textButton'
import { multiModalAIServices } from '@/features/stores/settings'

const YouTube = () => {
const youtubeApiKey = settingsStore((s) => s.youtubeApiKey)
Expand Down Expand Up @@ -98,8 +99,7 @@ const YouTube = () => {
})
}
disabled={
(selectAIService !== 'openai' &&
selectAIService !== 'anthropic') ||
!multiModalAIServices.includes(selectAIService as any) ||
slideMode ||
webSocketMode
}
Expand Down
51 changes: 19 additions & 32 deletions src/features/chat/aiChatFactory.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,41 @@
import { Message } from '@/features/messages/messages'
import { AIService, AIServiceConfig } from '@/features/constants/settings'
import { getAnthropicChatResponseStream } from './anthropicChat'
import { getDifyChatResponseStream } from './difyChat'
import { getGoogleChatResponseStream } from './googleChat'
import { getGroqChatResponseStream } from './groqChat'
import { AIService } from '@/features/constants/settings'
import { getLocalLLMChatResponseStream } from './localLLMChat'
import { getOpenAIChatResponseStream } from './openAiChat'
import { getDifyChatResponseStream } from './difyChat'
import { getVercelAIChatResponseStream } from './vercelAIChat'
import settingsStore from '@/features/stores/settings'

export async function getAIChatResponseStream(
service: AIService,
messages: Message[],
config: AIServiceConfig
messages: Message[]
): Promise<ReadableStream<string> | null> {
const ss = settingsStore.getState()

switch (service) {
case 'openai':
return getOpenAIChatResponseStream(
messages,
config.openai.key,
config.openai.model
)
case 'anthropic':
return getAnthropicChatResponseStream(
messages,
config.anthropic.key,
config.anthropic.model
)
case 'google':
return getGoogleChatResponseStream(
case 'groq':
return getVercelAIChatResponseStream(
messages,
config.google.key,
config.google.model
ss[`${service}Key`] ||
process.env[`NEXT_PUBLIC_${service.toUpperCase()}_KEY`] ||
'',
service,
ss.selectAIModel
)
case 'localLlm':
return getLocalLLMChatResponseStream(
messages,
config.localLlm.url,
config.localLlm.model
)
case 'groq':
return getGroqChatResponseStream(
messages,
config.groq.key,
config.groq.model
ss.localLlmUrl || process.env.NEXT_PUBLIC_LOCAL_LLM_URL || '',
ss.selectAIModel || process.env.NEXT_PUBLIC_LOCAL_LLM_MODEL || ''
)
case 'dify':
return getDifyChatResponseStream(
messages,
config.dify.key,
config.dify.url,
config.dify.conversationId
ss.difyKey || process.env.NEXT_PUBLIC_DIFY_KEY || '',
ss.difyUrl || process.env.NEXT_PUBLIC_DIFY_URL || '',
ss.difyConversationId
)
default:
throw new Error(`Unsupported AI service: ${service}`)
Expand Down
Loading
Loading