mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-04-01 22:39:26 +02: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({
|
return await this.ollamaService.getAvailableModels({
|
||||||
sort: reqData.sort,
|
sort: reqData.sort,
|
||||||
recommendedOnly: reqData.recommendedOnly,
|
recommendedOnly: reqData.recommendedOnly,
|
||||||
|
query: reqData.query || null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export default class SettingsController {
|
||||||
}
|
}
|
||||||
|
|
||||||
async models({ inertia }: HttpContext) {
|
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 installedModels = await this.ollamaService.getModels();
|
||||||
const chatSuggestionsEnabled = await KVStore.getValue('chat.suggestionsEnabled')
|
const chatSuggestionsEnabled = await KVStore.getValue('chat.suggestionsEnabled')
|
||||||
return inertia.render('settings/models', {
|
return inertia.render('settings/models', {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import axios from 'axios'
|
||||||
import { DownloadModelJob } from '#jobs/download_model_job'
|
import { DownloadModelJob } from '#jobs/download_model_job'
|
||||||
import { SERVICE_NAMES } from '../../constants/service_names.js'
|
import { SERVICE_NAMES } from '../../constants/service_names.js'
|
||||||
import transmit from '@adonisjs/transmit/services/main'
|
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 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')
|
const MODELS_CACHE_FILE = path.join(process.cwd(), 'storage', 'ollama-models-cache.json')
|
||||||
|
|
@ -155,9 +156,10 @@ export class OllamaService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAvailableModels(
|
async getAvailableModels(
|
||||||
{ sort, recommendedOnly }: { sort?: 'pulls' | 'name'; recommendedOnly?: boolean } = {
|
{ sort, recommendedOnly, query }: { sort?: 'pulls' | 'name'; recommendedOnly?: boolean, query: string | null } = {
|
||||||
sort: 'pulls',
|
sort: 'pulls',
|
||||||
recommendedOnly: false,
|
recommendedOnly: false,
|
||||||
|
query: null,
|
||||||
}
|
}
|
||||||
): Promise<NomadOllamaModel[] | null> {
|
): Promise<NomadOllamaModel[] | null> {
|
||||||
try {
|
try {
|
||||||
|
|
@ -171,7 +173,8 @@ export class OllamaService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!recommendedOnly) {
|
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)
|
// 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]] : [],
|
tags: model.tags && model.tags.length > 0 ? [model.tags[0]] : [],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
return this.fuseSearchModels(recommendedModels, query)
|
||||||
|
}
|
||||||
|
|
||||||
return recommendedModels
|
return recommendedModels
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|
@ -321,4 +329,16 @@ export class OllamaService {
|
||||||
})
|
})
|
||||||
logger.info(`[OllamaService] Download progress for model "${model}": ${percent}%`)
|
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({
|
vine.object({
|
||||||
sort: vine.enum(['pulls', 'name'] as const).optional(),
|
sort: vine.enum(['pulls', 'name'] as const).optional(),
|
||||||
recommendedOnly: vine.boolean().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 () => {
|
return catchInternal(async () => {
|
||||||
const response = await this.client.get<NomadOllamaModel[]>('/ollama/models', {
|
const response = await this.client.get<NomadOllamaModel[]>('/ollama/models', {
|
||||||
params: { sort: 'pulls', recommendedOnly: true },
|
params: { sort: 'pulls', recommendedOnly, query },
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
})()
|
})()
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
|
||||||
|
|
||||||
const { data: recommendedModels, isLoading: isLoadingRecommendedModels } = useQuery({
|
const { data: recommendedModels, isLoading: isLoadingRecommendedModels } = useQuery({
|
||||||
queryKey: ['recommended-ollama-models'],
|
queryKey: ['recommended-ollama-models'],
|
||||||
queryFn: () => api.getRecommendedModels(),
|
queryFn: () => api.getAvailableModels(null, true),
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,10 @@ import { ModelResponse } from 'ollama'
|
||||||
import { SERVICE_NAMES } from '../../../constants/service_names'
|
import { SERVICE_NAMES } from '../../../constants/service_names'
|
||||||
import Switch from '~/components/inputs/Switch'
|
import Switch from '~/components/inputs/Switch'
|
||||||
import StyledSectionHeader from '~/components/StyledSectionHeader'
|
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: {
|
export default function ModelsPage(props: {
|
||||||
models: {
|
models: {
|
||||||
|
|
@ -26,10 +29,30 @@ export default function ModelsPage(props: {
|
||||||
const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA)
|
const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA)
|
||||||
const { addNotification } = useNotifications()
|
const { addNotification } = useNotifications()
|
||||||
const { openModal, closeAllModals } = useModals()
|
const { openModal, closeAllModals } = useModals()
|
||||||
|
const { debounce } = useDebounce()
|
||||||
const [chatSuggestionsEnabled, setChatSuggestionsEnabled] = useState(
|
const [chatSuggestionsEnabled, setChatSuggestionsEnabled] = useState(
|
||||||
props.models.settings.chatSuggestionsEnabled
|
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) {
|
async function handleInstallModel(modelName: string) {
|
||||||
try {
|
try {
|
||||||
const res = await api.downloadModel(modelName)
|
const res = await api.downloadModel(modelName)
|
||||||
|
|
@ -144,8 +167,22 @@ export default function ModelsPage(props: {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<StyledSectionHeader title="Models" className="mt-12 mb-4" />
|
<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>
|
<StyledTable<NomadOllamaModel>
|
||||||
className="font-semibold"
|
className="font-semibold mt-4"
|
||||||
rowLines={true}
|
rowLines={true}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
|
|
@ -169,7 +206,8 @@ export default function ModelsPage(props: {
|
||||||
title: 'Last Updated',
|
title: 'Last Updated',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
data={props.models.availableModels || []}
|
data={availableModels || []}
|
||||||
|
loading={isLoading}
|
||||||
expandable={{
|
expandable={{
|
||||||
expandedRowRender: (record) => (
|
expandedRowRender: (record) => (
|
||||||
<div className="pl-14">
|
<div className="pl-14">
|
||||||
|
|
|
||||||
10
admin/package-lock.json
generated
10
admin/package-lock.json
generated
|
|
@ -43,6 +43,7 @@
|
||||||
"dockerode": "^4.0.7",
|
"dockerode": "^4.0.7",
|
||||||
"edge.js": "^6.2.1",
|
"edge.js": "^6.2.1",
|
||||||
"fast-xml-parser": "^5.2.5",
|
"fast-xml-parser": "^5.2.5",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
"luxon": "^3.6.1",
|
"luxon": "^3.6.1",
|
||||||
"maplibre-gl": "^4.7.1",
|
"maplibre-gl": "^4.7.1",
|
||||||
"mysql2": "^3.14.1",
|
"mysql2": "^3.14.1",
|
||||||
|
|
@ -8350,6 +8351,15 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/generate-function": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@
|
||||||
"dockerode": "^4.0.7",
|
"dockerode": "^4.0.7",
|
||||||
"edge.js": "^6.2.1",
|
"edge.js": "^6.2.1",
|
||||||
"fast-xml-parser": "^5.2.5",
|
"fast-xml-parser": "^5.2.5",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
"luxon": "^3.6.1",
|
"luxon": "^3.6.1",
|
||||||
"maplibre-gl": "^4.7.1",
|
"maplibre-gl": "^4.7.1",
|
||||||
"mysql2": "^3.14.1",
|
"mysql2": "^3.14.1",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user