Skip to content

Commit

Permalink
ollamanager
Browse files Browse the repository at this point in the history
  • Loading branch information
thot-experiment committed Aug 28, 2024
1 parent 79e26b8 commit 6e50160
Show file tree
Hide file tree
Showing 7 changed files with 4,315 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ollama
93 changes: 93 additions & 0 deletions thoth_front_end/downloadAndUnzip.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { promisify } from 'util'
import path from 'path'
import fs from 'fs'
import stream from 'stream'
import yauzl from 'yauzl'

const pipeline = promisify(stream.pipeline)

const update_progress = (message) => {
process.stdout.clearLine()
process.stdout.cursorTo(0)
process.stdout.write(message)
}

const download_and_unzip = async (url, outputPath) => {
const tempPath = path.join(outputPath, 'ollama.zip')

try {
// Download the file
const response = await fetch(url)
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
const contentLength = response.headers.get('Content-Length')
let downloadedBytes = 0

const fileStream = fs.createWriteStream(tempPath)
const reader = response.body.getReader()

while (true) {
const { done, value } = await reader.read()
if (done) break
fileStream.write(value)
downloadedBytes += value.length
if (contentLength) {
const percentCompleted = Math.round((downloadedBytes * 100) / contentLength)
update_progress(`Download progress: ${percentCompleted}%`)
}
}

fileStream.end()
await new Promise(resolve => fileStream.on('finish', resolve))
console.log('\nDownload completed')

// Unzip the file
return new Promise((resolve, reject) => {
yauzl.open(tempPath, { lazyEntries: true }, (err, zipfile) => {
if (err) return reject(err)

let totalEntries = zipfile.entryCount
let processedEntries = 0

zipfile.on('entry', (entry) => {
const entryPath = path.join(outputPath, entry.fileName)
if (/\/$/.test(entry.fileName)) {
// Directory entry
fs.mkdirSync(entryPath, { recursive: true })
zipfile.readEntry()
} else {
// File entry
// it seems like some file entries that are nested come before the
// relevant directory entries, or the directory entries don't come at all?
const dir = path.dirname(entryPath)
//console.log('fileent! '+entry.fileName, dir)
fs.mkdirSync(dir , { recursive: true })
zipfile.openReadStream(entry, (err, readStream) => {
if (err) return reject(err)
const writeStream = fs.createWriteStream(entryPath)
readStream.pipe(writeStream)
writeStream.on('finish', () => {
processedEntries++
const percentCompleted = Math.round((processedEntries * 100) / totalEntries)
update_progress(`Unzip progress: ${percentCompleted}%`)
zipfile.readEntry()
})
})
}
})

zipfile.on('end', () => {
fs.unlinkSync(tempPath)
console.log('\nUnzip completed')
resolve()
})

zipfile.readEntry()
})
})
} catch (error) {
console.error('Error:', error.message)
throw error
}
}

export default download_and_unzip
17 changes: 17 additions & 0 deletions thoth_front_end/main.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import { execSync, spawn, spawnSync } from 'child_process'
import { app, BrowserWindow, globalShortcut } from 'electron'
import path from 'path'
//TODO fix the python stuff to use the promise lib
import fs from 'fs'
import summon_ollama from './ollamanager.mjs'

let ollama_config = await fs.promises.readFile('./ollama.json')
.then(a => JSON.parse(a))
.catch(_ => {})

if (!ollama_config) {
console.log('configuration (ollama.json) not found, using sane defaults')
ollama_config = {}

} else {
console.log('found valid ollama.json\n')
console.log({ollama_config},'\n')
}

const ollama_process = await summon_ollama(ollama_config)

let main_window
let python_process
Expand Down
149 changes: 149 additions & 0 deletions thoth_front_end/ollamanager.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import child_process from 'child_process'
import { promisify } from 'util'
import path from 'path'
import http from 'http'
import fs from 'fs/promises'
import download_and_unzip from './downloadAndUnzip.mjs'
import {version} from 'os'

const expected_version = '0.3.7'

const exec = promisify(child_process.exec)
const nul = a => null

const get_existing_ollama = async (ollama_path = 'ollama', hostname='') => {
let status = (await exec(`${path.resolve(ollama_path)} --version`).then(a => a.stdout))
//console.log({status})
ollama_path = (await
exec(`where ${ollama_path}`)
.then(a => a.toString())
.catch(a => null)
) || ollama_path
let running = !status.match(/Warning: could not connect to a running Ollama instance/i)
//TODO this is gross but needed to handle the case where ollama is running locally managed
//on a diff port in order to not conflict with an existing ollama install
if (!running) running = await fetch(hostname+'/api/version')
.then(_ => true)
.catch(_ => false)
let version = status.match(/version is (.+?)$/mi)?.[1]
return {running, ollama_path, version}
}

const version_info_blurb = version => version === expected_version?
`it is the correct version (v${expected_version})`:
`it is *not* the expected version (expected: v${expected_version}, found: v${version})\nthis is probably fine but if there are issues set "force_local":true in \`ollama.json\` to force Thoth to download it's own copy`

//TODO think about using the API, though i really think this is fine since the whole thing
//is relying on a particular release being available and named a particular thing by the authors
let get_release_artifacts = (release_tag) => fetch(`https://github.com/ollama/ollama/releases/expanded_assets/${release_tag}`)
.then(a => a.text())
.then(html => [...html.matchAll(/\<.*?a.*?href="(.+?)"/gim)].map(m => {
const github_root = 'https://github.com'
const artifact_url = m[1]
const filename = artifact_url.split('/').at(-1)
return {url: github_root+artifact_url, filename}
}))
//.catch(e => console.error(e))

const summon_ollama = async ({
force_local,
//force_manage,
path:ollama_path = '../ollama/bin/ollama.exe',
hostname = 'http://localhost:11434'
}) => {
if (force_local) {
ollama_path = '../ollama/bin/ollama.exe'
hostname = 'http://localhost:12434'
}
ollama_path = path.resolve(ollama_path)
let onprogress
let process
let status = {ready: false}

let existing_ollama = await fetch(hostname+'/api/version')
.then(a => a.json())
.then(a => (a.running=true,a))
.catch(a => ({}))

if (existing_ollama.version) {
const version_info = version_info_blurb(existing_ollama.version)
console.log(`found running ollama server at ${hostname}\n${version_info}`)
if (force_local) console.warn(`force_local is set to true in ollama.json but a server is already running, this is highly unusual but we'll try to roll with it`)
} else {
console.log('\nlooking for existing ollama installation...\n')
existing_ollama = await get_existing_ollama(ollama_path, hostname)
.catch(error => ({error}))
}


if (existing_ollama.error) {
console.log(`ollama is not available, thoth will now attempt to install it, this may take some time`)
const ollama_dir = '../ollama'
await fs.mkdir(ollama_dir).catch(a => null)
ollama_path = path.resolve('../ollama/bin/ollama.exe')
//TODO actually implement this
//if (!ollama_path) console.log('if you have ollama installed but NOT on PATH you may configure it\'s location by setting "path":"C:\\your_ollama_location\\ollama.exe" in `ollama.json`')
console.log('checking github.com/ollama/ollama for binaries')
const release = await get_release_artifacts(`v${expected_version}`)
.then(releases => {
//we only support windows right now
return releases.find(({filename}) => filename.match(/windows-amd64.zip/i))
})
.catch(error => {
throw `unable to reach github or some other such nonsense, this is too confusing for me, you gotta fix it yourself :(\nadditional info:${error}`
})

console.log(`found a suitable binary @ ${release.url}! downloading...`)
await download_and_unzip(release.url, ollama_dir)
existing_ollama = await get_existing_ollama(ollama_path)
.catch(error => ({error}))
}

if (existing_ollama.error) throw existing_ollama.error

//console.log({existing_ollama})

if (existing_ollama.running) {
//TODO actually check that the API is available here
console.log('everything looks good, ollama is running and accessible')
} else {
const version_info = version_info_blurb(existing_ollama.version)
console.log(`found ollama at ${existing_ollama.ollama_path}\n${version_info}`)
console.log(`ollama doesn't appear to be running, starting now`)
const env = {
//...process.env,
OLLAMA_HOST: '127.0.0.1:'+hostname.split(':')
.map(a => a.trim())
.filter(a => a)
.at(-1)
}
console.log({env})
try {
process = child_process.spawn(`${path.resolve(ollama_path)}`, ['serve'], {env})
console.log(ollama_path)
} catch (e) {
console.warn(e)
}
//TODO we should figure out how to listen properly here, but i'm just going
//to throw a wait in here to fix the race condition
//await new Promise(r => setTimeout(r,1000))
existing_ollama = await get_existing_ollama(ollama_path, hostname)
.catch(error => ({error}))
if (!existing_ollama.running) throw 'did not manage to start ollama' + JSON.stringify(existing_ollama, null, 2)
console.log(`yay, everything is working, ollama running in the background (v${existing_ollama.version})`)
}

return {
version: existing_ollama.version,
ollama_path: existing_ollama.ollama_path,
hostname,
//process,
/*
get status() {
return status
}
*/
}
}

export default summon_ollama
Loading

0 comments on commit 6e50160

Please sign in to comment.