mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
feat(AI): add fuzzy search to models list
This commit is contained in:
parent
1952d585d3
commit
d4cbc0c2d5
|
|
@ -18,6 +18,7 @@ export default class OllamaController {
|
|||
return await this.ollamaService.getAvailableModels({
|
||||
sort: reqData.sort,
|
||||
recommendedOnly: reqData.recommendedOnly,
|
||||
query: reqData.query || null,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export default class SettingsController {
|
|||
}
|
||||
|
||||
async models({ inertia }: HttpContext) {
|
||||
const availableModels = await this.ollamaService.getAvailableModels({ sort: 'pulls', recommendedOnly: false });
|
||||
const availableModels = await this.ollamaService.getAvailableModels({ sort: 'pulls', recommendedOnly: false, query: null });
|
||||
const installedModels = await this.ollamaService.getModels();
|
||||
const chatSuggestionsEnabled = await KVStore.getValue('chat.suggestionsEnabled')
|
||||
return inertia.render('settings/models', {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import axios from 'axios'
|
|||
import { DownloadModelJob } from '#jobs/download_model_job'
|
||||
import { SERVICE_NAMES } from '../../constants/service_names.js'
|
||||
import transmit from '@adonisjs/transmit/services/main'
|
||||
import Fuse, { IFuseOptions } from 'fuse.js'
|
||||
|
||||
const NOMAD_MODELS_API_BASE_URL = 'https://api.projectnomad.us/api/v1/ollama/models'
|
||||
const MODELS_CACHE_FILE = path.join(process.cwd(), 'storage', 'ollama-models-cache.json')
|
||||
|
|
@ -155,9 +156,10 @@ export class OllamaService {
|
|||
}
|
||||
|
||||
async getAvailableModels(
|
||||
{ sort, recommendedOnly }: { sort?: 'pulls' | 'name'; recommendedOnly?: boolean } = {
|
||||
{ sort, recommendedOnly, query }: { sort?: 'pulls' | 'name'; recommendedOnly?: boolean, query: string | null } = {
|
||||
sort: 'pulls',
|
||||
recommendedOnly: false,
|
||||
query: null,
|
||||
}
|
||||
): Promise<NomadOllamaModel[] | null> {
|
||||
try {
|
||||
|
|
@ -171,7 +173,8 @@ export class OllamaService {
|
|||
}
|
||||
|
||||
if (!recommendedOnly) {
|
||||
return models
|
||||
const filteredModels = query ? this.fuseSearchModels(models, query) : models
|
||||
return filteredModels
|
||||
}
|
||||
|
||||
// If recommendedOnly is true, only return the first three models (if sorted by pulls, these will be the top 3)
|
||||
|
|
@ -185,6 +188,11 @@ export class OllamaService {
|
|||
tags: model.tags && model.tags.length > 0 ? [model.tags[0]] : [],
|
||||
}
|
||||
})
|
||||
|
||||
if (query) {
|
||||
return this.fuseSearchModels(recommendedModels, query)
|
||||
}
|
||||
|
||||
return recommendedModels
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
|
|
@ -321,4 +329,16 @@ export class OllamaService {
|
|||
})
|
||||
logger.info(`[OllamaService] Download progress for model "${model}": ${percent}%`)
|
||||
}
|
||||
|
||||
private fuseSearchModels(models: NomadOllamaModel[], query: string): NomadOllamaModel[] {
|
||||
const options: IFuseOptions<NomadOllamaModel> = {
|
||||
ignoreDiacritics: true,
|
||||
keys: ['name', 'description', 'tags.tag'],
|
||||
threshold: 0.3, // lower threshold for stricter matching
|
||||
}
|
||||
|
||||
const fuse = new Fuse(models, options)
|
||||
|
||||
return fuse.search(query).map(result => result.item)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,5 +17,6 @@ export const getAvailableModelsSchema = vine.compile(
|
|||
vine.object({
|
||||
sort: vine.enum(['pulls', 'name'] as const).optional(),
|
||||
recommendedOnly: vine.boolean().optional(),
|
||||
query: vine.string().trim().optional(),
|
||||
})
|
||||
)
|
||||
|
|
|
|||
|
|
@ -161,10 +161,10 @@ class API {
|
|||
})()
|
||||
}
|
||||
|
||||
async getRecommendedModels(): Promise<NomadOllamaModel[] | undefined> {
|
||||
async getAvailableModels(query: string | null, recommendedOnly: boolean): Promise<NomadOllamaModel[] | undefined> {
|
||||
return catchInternal(async () => {
|
||||
const response = await this.client.get<NomadOllamaModel[]>('/ollama/models', {
|
||||
params: { sort: 'pulls', recommendedOnly: true },
|
||||
params: { sort: 'pulls', recommendedOnly, query },
|
||||
})
|
||||
return response.data
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
|
|||
|
||||
const { data: recommendedModels, isLoading: isLoadingRecommendedModels } = useQuery({
|
||||
queryKey: ['recommended-ollama-models'],
|
||||
queryFn: () => api.getRecommendedModels(),
|
||||
queryFn: () => api.getAvailableModels(null, true),
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ import { ModelResponse } from 'ollama'
|
|||
import { SERVICE_NAMES } from '../../../constants/service_names'
|
||||
import Switch from '~/components/inputs/Switch'
|
||||
import StyledSectionHeader from '~/components/StyledSectionHeader'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import Input from '~/components/inputs/Input'
|
||||
import { IconSearch } from '@tabler/icons-react'
|
||||
import useDebounce from '~/hooks/useDebounce'
|
||||
|
||||
export default function ModelsPage(props: {
|
||||
models: {
|
||||
|
|
@ -26,10 +29,30 @@ export default function ModelsPage(props: {
|
|||
const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA)
|
||||
const { addNotification } = useNotifications()
|
||||
const { openModal, closeAllModals } = useModals()
|
||||
const { debounce } = useDebounce()
|
||||
const [chatSuggestionsEnabled, setChatSuggestionsEnabled] = useState(
|
||||
props.models.settings.chatSuggestionsEnabled
|
||||
)
|
||||
|
||||
const [query, setQuery] = useState('')
|
||||
const [queryUI, setQueryUI] = useState('')
|
||||
|
||||
const debouncedSetQuery = debounce((val: string) => {
|
||||
setQuery(val)
|
||||
}, 300)
|
||||
|
||||
const { data: availableModels, isLoading } = useQuery({
|
||||
queryKey: ['ollama', 'availableModels', query],
|
||||
queryFn: async () => {
|
||||
const res = await api.getAvailableModels(query, false)
|
||||
if (!res) {
|
||||
return []
|
||||
}
|
||||
return res
|
||||
},
|
||||
initialData: props.models.availableModels,
|
||||
})
|
||||
|
||||
async function handleInstallModel(modelName: string) {
|
||||
try {
|
||||
const res = await api.downloadModel(modelName)
|
||||
|
|
@ -144,8 +167,22 @@ export default function ModelsPage(props: {
|
|||
</div>
|
||||
</div>
|
||||
<StyledSectionHeader title="Models" className="mt-12 mb-4" />
|
||||
<div className="flex justify-start mt-4">
|
||||
<Input
|
||||
name="search"
|
||||
label=""
|
||||
placeholder="Search language models.."
|
||||
value={queryUI}
|
||||
onChange={(e) => {
|
||||
setQueryUI(e.target.value)
|
||||
debouncedSetQuery(e.target.value)
|
||||
}}
|
||||
className="w-1/3"
|
||||
leftIcon={<IconSearch className="w-5 h-5 text-gray-400" />}
|
||||
/>
|
||||
</div>
|
||||
<StyledTable<NomadOllamaModel>
|
||||
className="font-semibold"
|
||||
className="font-semibold mt-4"
|
||||
rowLines={true}
|
||||
columns={[
|
||||
{
|
||||
|
|
@ -169,7 +206,8 @@ export default function ModelsPage(props: {
|
|||
title: 'Last Updated',
|
||||
},
|
||||
]}
|
||||
data={props.models.availableModels || []}
|
||||
data={availableModels || []}
|
||||
loading={isLoading}
|
||||
expandable={{
|
||||
expandedRowRender: (record) => (
|
||||
<div className="pl-14">
|
||||
|
|
|
|||
10
admin/package-lock.json
generated
10
admin/package-lock.json
generated
|
|
@ -43,6 +43,7 @@
|
|||
"dockerode": "^4.0.7",
|
||||
"edge.js": "^6.2.1",
|
||||
"fast-xml-parser": "^5.2.5",
|
||||
"fuse.js": "^7.1.0",
|
||||
"luxon": "^3.6.1",
|
||||
"maplibre-gl": "^4.7.1",
|
||||
"mysql2": "^3.14.1",
|
||||
|
|
@ -8350,6 +8351,15 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/fuse.js": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
|
||||
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@
|
|||
"dockerode": "^4.0.7",
|
||||
"edge.js": "^6.2.1",
|
||||
"fast-xml-parser": "^5.2.5",
|
||||
"fuse.js": "^7.1.0",
|
||||
"luxon": "^3.6.1",
|
||||
"maplibre-gl": "^4.7.1",
|
||||
"mysql2": "^3.14.1",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user