Skip to content

Commit

Permalink
Vercel AI SDKを導入して各AIサービスの呼び出し処理を統一化
Browse files Browse the repository at this point in the history
  • Loading branch information
tegnike committed Aug 30, 2024
1 parent 5b1f101 commit fbfe2da
Show file tree
Hide file tree
Showing 27 changed files with 1,834 additions and 983 deletions.
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

0 comments on commit fbfe2da

Please sign in to comment.