feat: add ollama settings

This commit is contained in:
kangfenmao 2024-08-06 20:38:01 +08:00
parent 167988927b
commit 5edb53ef7d
12 changed files with 118 additions and 15 deletions

View File

@ -1,3 +1,4 @@
import { useAppInit } from '@renderer/hooks/useAppInit'
import { message, Modal } from 'antd' import { message, Modal } from 'antd'
import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react' import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react'
@ -30,6 +31,8 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
const [messageApi, messageContextHolder] = message.useMessage() const [messageApi, messageContextHolder] = message.useMessage()
const [modal, modalContextHolder] = Modal.useModal() const [modal, modalContextHolder] = Modal.useModal()
useAppInit()
useEffect(() => { useEffect(() => {
window.message = messageApi window.message = messageApi
window.modal = modal window.modal = modal

View File

@ -0,0 +1,18 @@
import store, { useAppSelector } from '@renderer/store'
import { setOllamaKeepAliveTime } from '@renderer/store/llm'
import { useDispatch } from 'react-redux'
export function useOllamaSettings() {
const settings = useAppSelector((state) => state.llm.settings.ollama)
const dispatch = useDispatch()
return { ...settings, setKeepAliveTime: (time: number) => dispatch(setOllamaKeepAliveTime(time)) }
}
export function getOllamaSettings() {
return store.getState().llm.settings.ollama
}
export function getOllamaKeepAliveTime() {
return store.getState().llm.settings.ollama.keepAliveTime + 'm'
}

View File

@ -2,12 +2,10 @@ import { Assistant, Topic } from '@renderer/types'
import { find } from 'lodash' import { find } from 'lodash'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
const activeTopicsMap = new Map<string, Topic>() let _activeTopic: Topic
export function useActiveTopic(assistant: Assistant) { export function useActiveTopic(assistant: Assistant) {
const [activeTopic, setActiveTopic] = useState(activeTopicsMap.get(assistant.id) || assistant?.topics[0]) const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0])
activeTopicsMap.set(assistant.id, activeTopic)
useEffect(() => { useEffect(() => {
// activeTopic not in assistant.topics // activeTopic not in assistant.topics

View File

@ -196,6 +196,12 @@ const resources = {
italian: 'Italian', italian: 'Italian',
portuguese: 'Portuguese', portuguese: 'Portuguese',
arabic: 'Arabic' arabic: 'Arabic'
},
ollama: {
title: 'Ollama',
'keep_alive_time.title': 'Keep Alive Time',
'keep_alive_time.placeholder': 'Minutes',
'keep_alive_time.description': 'The time in minutes to keep the connection alive, default is 5 minutes.'
} }
} }
}, },
@ -393,6 +399,12 @@ const resources = {
italian: '意大利文', italian: '意大利文',
portuguese: '葡萄牙文', portuguese: '葡萄牙文',
arabic: '阿拉伯文' arabic: '阿拉伯文'
},
ollama: {
title: 'Ollama',
'keep_alive_time.title': '保持活跃时间',
'keep_alive_time.placeholder': '分钟',
'keep_alive_time.description': '对话后模型在内存中保持的时间默认5分钟'
} }
} }
} }

View File

@ -40,7 +40,7 @@ const SelectModelDropdown: FC<Props & PropsWithChildren> = ({ children, model, o
menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }} menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }}
trigger={['click']} trigger={['click']}
arrow arrow
placement="bottomCenter" placement="bottom"
overlayClassName="chat-nav-dropdown" overlayClassName="chat-nav-dropdown"
{...props}> {...props}>
{children} {children}

View File

@ -187,7 +187,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
<Tag style={{ cursor: 'pointer' }}>{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}</Tag> <Tag style={{ cursor: 'pointer' }}>{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}</Tag>
</Tooltip> </Tooltip>
<Tooltip title={t('chat.input.estimated_tokens.tip')}> <Tooltip title={t('chat.input.estimated_tokens.tip')}>
<Tag style={{ cursor: 'pointer' }}>{`${inputTokenCount} / ${estimateTokenCount}`}</Tag> <Tag style={{ cursor: 'pointer' }}> {`${inputTokenCount} / ${estimateTokenCount}`}</Tag>
</Tooltip> </Tooltip>
</TextCount> </TextCount>
)} )}

View File

@ -19,6 +19,7 @@ import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import OllamSettings from '../providers/OllamaSettings'
import { SettingContainer, SettingSubtitle, SettingTitle } from '.' import { SettingContainer, SettingSubtitle, SettingTitle } from '.'
import AddModelPopup from './AddModelPopup' import AddModelPopup from './AddModelPopup'
import EditModelsPopup from './EditModelsPopup' import EditModelsPopup from './EditModelsPopup'
@ -126,6 +127,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
/> />
{apiEditable && <Button onClick={onReset}>{t('settings.provider.api.url.reset')}</Button>} {apiEditable && <Button onClick={onReset}>{t('settings.provider.api.url.reset')}</Button>}
</Space.Compact> </Space.Compact>
{provider.id === 'ollama' && <OllamSettings />}
<SettingSubtitle>{t('common.models')}</SettingSubtitle> <SettingSubtitle>{t('common.models')}</SettingSubtitle>
{Object.keys(modelGroups).map((group) => ( {Object.keys(modelGroups).map((group) => (
<Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small"> <Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small">
@ -182,14 +184,14 @@ const ModelListHeader = styled.div`
align-items: center; align-items: center;
` `
const HelpTextRow = styled.div` export const HelpTextRow = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 5px 0; padding: 5px 0;
` `
const HelpText = styled.div` export const HelpText = styled.div`
font-size: 11px; font-size: 11px;
color: var(--color-text); color: var(--color-text);
opacity: 0.4; opacity: 0.4;

View File

@ -0,0 +1,35 @@
import { useOllamaSettings } from '@renderer/hooks/useOllama'
import { InputNumber } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingSubtitle } from '../components'
import { HelpText, HelpTextRow } from '../components/ProviderSetting'
const OllamSettings: FC = () => {
const { keepAliveTime, setKeepAliveTime } = useOllamaSettings()
const [keepAliveMinutes, setKeepAliveMinutes] = useState(keepAliveTime)
const { t } = useTranslation()
return (
<Container>
<SettingSubtitle>{t('ollama.keep_alive_time.title')}</SettingSubtitle>
<InputNumber
style={{ width: '100%' }}
value={keepAliveMinutes}
onChange={(e) => setKeepAliveMinutes(Number(e))}
onBlur={() => setKeepAliveTime(keepAliveMinutes)}
suffix={t('ollama.keep_alive_time.placeholder')}
step={5}
/>
<HelpTextRow>
<HelpText>{t('ollama.keep_alive_time.description')}</HelpText>
</HelpTextRow>
</Container>
)
}
const Container = styled.div``
export default OllamSettings

View File

@ -1,5 +1,6 @@
import Anthropic from '@anthropic-ai/sdk' import Anthropic from '@anthropic-ai/sdk'
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources' import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama'
import { Assistant, Message, Provider, Suggestion } from '@renderer/types' import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
import { getAssistantSettings, removeQuotes } from '@renderer/utils' import { getAssistantSettings, removeQuotes } from '@renderer/utils'
import { sum, takeRight } from 'lodash' import { sum, takeRight } from 'lodash'
@ -26,6 +27,10 @@ export default class ProviderSDK {
return this.provider.id === 'anthropic' return this.provider.id === 'anthropic'
} }
private get keepAliveTime() {
return this.provider.id === 'ollama' ? getOllamaKeepAliveTime() : undefined
}
public async completions( public async completions(
messages: Message[], messages: Message[],
assistant: Assistant, assistant: Assistant,
@ -61,11 +66,13 @@ export default class ProviderSDK {
}) })
) )
} else { } else {
// @ts-ignore key is not typed
const stream = await this.openaiSdk.chat.completions.create({ const stream = await this.openaiSdk.chat.completions.create({
model: model.id, model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[], messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
stream: true, stream: true,
temperature: assistant?.settings?.temperature temperature: assistant?.settings?.temperature,
keep_alive: this.keepAliveTime
}) })
for await (const chunk of stream) { for await (const chunk of stream) {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
@ -92,10 +99,12 @@ export default class ProviderSDK {
}) })
return response.content[0].type === 'text' ? response.content[0].text : '' return response.content[0].type === 'text' ? response.content[0].text : ''
} else { } else {
// @ts-ignore key is not typed
const response = await this.openaiSdk.chat.completions.create({ const response = await this.openaiSdk.chat.completions.create({
model: model.id, model: model.id,
messages: messages as ChatCompletionMessageParam[], messages: messages as ChatCompletionMessageParam[],
stream: false stream: false,
keep_alive: this.keepAliveTime
}) })
return response.choices[0].message?.content || '' return response.choices[0].message?.content || ''
} }
@ -124,11 +133,13 @@ export default class ProviderSDK {
return message.content[0].type === 'text' ? message.content[0].text : null return message.content[0].type === 'text' ? message.content[0].text : null
} else { } else {
// @ts-ignore key is not typed
const response = await this.openaiSdk.chat.completions.create({ const response = await this.openaiSdk.chat.completions.create({
model: model.id, model: model.id,
messages: [systemMessage, ...userMessages] as ChatCompletionMessageParam[], messages: [systemMessage, ...userMessages] as ChatCompletionMessageParam[],
stream: false, stream: false,
max_tokens: 50 max_tokens: 50,
keep_alive: this.keepAliveTime
}) })
return removeQuotes(response.choices[0].message?.content || '') return removeQuotes(response.choices[0].message?.content || '')

View File

@ -22,7 +22,7 @@ const persistedReducer = persistReducer(
{ {
key: 'cherry-studio', key: 'cherry-studio',
storage, storage,
version: 18, version: 19,
blacklist: ['runtime'], blacklist: ['runtime'],
migrate migrate
}, },

View File

@ -3,11 +3,18 @@ import { SYSTEM_MODELS } from '@renderer/config/models'
import { Model, Provider } from '@renderer/types' import { Model, Provider } from '@renderer/types'
import { uniqBy } from 'lodash' import { uniqBy } from 'lodash'
type LlmSettings = {
ollama: {
keepAliveTime: number
}
}
export interface LlmState { export interface LlmState {
providers: Provider[] providers: Provider[]
defaultModel: Model defaultModel: Model
topicNamingModel: Model topicNamingModel: Model
translateModel: Model translateModel: Model
settings: LlmSettings
} }
const initialState: LlmState = { const initialState: LlmState = {
@ -132,7 +139,12 @@ const initialState: LlmState = {
isSystem: true, isSystem: true,
enabled: false enabled: false
} }
] ],
settings: {
ollama: {
keepAliveTime: 0
}
}
} }
const settingsSlice = createSlice({ const settingsSlice = createSlice({
@ -179,6 +191,9 @@ const settingsSlice = createSlice({
}, },
setTranslateModel: (state, action: PayloadAction<{ model: Model }>) => { setTranslateModel: (state, action: PayloadAction<{ model: Model }>) => {
state.translateModel = action.payload.model state.translateModel = action.payload.model
},
setOllamaKeepAliveTime: (state, action: PayloadAction<number>) => {
state.settings.ollama.keepAliveTime = action.payload
} }
} }
}) })
@ -192,7 +207,8 @@ export const {
removeModel, removeModel,
setDefaultModel, setDefaultModel,
setTopicNamingModel, setTopicNamingModel,
setTranslateModel setTranslateModel,
setOllamaKeepAliveTime
} = settingsSlice.actions } = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer

View File

@ -272,11 +272,19 @@ const migrateConfig = {
} }
} }
}, },
'18': (state: RootState) => { '19': (state: RootState) => {
return { return {
...state, ...state,
agents: { agents: {
agents: [] agents: []
},
llm: {
...state.llm,
settings: {
ollama: {
keepAliveTime: 5
}
}
} }
} }
} }