Skip to content

Commit

Permalink
feat: Add Ollama Model Management Features (#163)
Browse files Browse the repository at this point in the history
* fix: CLI hardcoded version and solving issues with containers and images

* feat: new global var to deal with compatible version

* Release v0.10.0-beta.0 [skip ci]

* fix: removing default value from .env.example

* feat: new update ollama command

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@genlayer.com>
  • Loading branch information
epsjunior and github-actions[bot] authored Jan 7, 2025
1 parent 13cfe51 commit e48e31e
Show file tree
Hide file tree
Showing 11 changed files with 264 additions and 115 deletions.
6 changes: 4 additions & 2 deletions src/commands/general/init.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import inquirer from "inquirer";

import {ISimulatorService} from "../../lib/interfaces/ISimulatorService";
import {AI_PROVIDERS_CONFIG, AiProviders} from "../../lib/config/simulator";
import { OllamaAction } from "../update/ollama";

export interface InitActionOptions {
numValidators: number;
headless: boolean;
Expand Down Expand Up @@ -179,7 +180,8 @@ export async function initAction(options: InitActionOptions, simulatorService: I
// Ollama doesn't need changes in configuration, we just run it
if (selectedLlmProviders.includes("ollama")) {
console.log("Pulling llama3 from Ollama...");
await simulatorService.pullOllamaModel();
const ollamaAction = new OllamaAction();
await ollamaAction.updateModel("llama3");
}

// Initializing validators
Expand Down
26 changes: 26 additions & 0 deletions src/commands/update/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Command } from "commander";
import { OllamaAction } from "./ollama";

export function initializeUpdateCommands(program: Command) {
const updateCommand = program
.command("update")
.description("Update resources like models or configurations");

updateCommand
.command("ollama")
.description("Manage Ollama models (update or remove)")
.option("--model [model-name]", "Specify the model to update or remove")
.option("--remove", "Remove the specified model instead of updating")
.action(async (options) => {
const modelName = options.model || "default-model";
const ollamaAction = new OllamaAction();

if (options.remove) {
await ollamaAction.removeModel(modelName);
} else {
await ollamaAction.updateModel(modelName);
}
});

return program;
}
42 changes: 42 additions & 0 deletions src/commands/update/ollama.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Docker from "dockerode"

export class OllamaAction {
private docker: Docker;

constructor() {
this.docker = new Docker();
}

async updateModel(modelName: string) {
await this.executeModelCommand("pull", modelName, `Model "${modelName}" updated successfully`);
}

async removeModel(modelName: string) {
await this.executeModelCommand("rm", modelName, `Model "${modelName}" removed successfully`);
}

private async executeModelCommand(command: string, modelName: string, successMessage: string) {
try {
const ollamaContainer = this.docker.getContainer("ollama");
const exec = await ollamaContainer.exec({
Cmd: ["ollama", command, modelName],
AttachStdout: true,
AttachStderr: true,
});
const stream = await exec.start({ Detach: false, Tty: false });

stream.on("data", (chunk: any) => {
console.log(chunk.toString());
});

await new Promise<void>((resolve, reject) => {
stream.on("end", resolve);
stream.on("error", reject);
});

console.log(successMessage);
} catch (error) {
console.error(`Error executing command "${command}" on model "${modelName}":`, error);
}
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { initializeGeneralCommands } from "../src/commands/general";
import { initializeKeygenCommands } from "../src/commands/keygen";
import { initializeContractsCommands } from "../src/commands/contracts";
import { initializeConfigCommands } from "../src/commands/config";
import { initializeUpdateCommands } from "../src/commands/update";

export function initializeCLI() {
program.version(version).description(CLI_DESCRIPTION);
initializeGeneralCommands(program);
initializeKeygenCommands(program);
initializeContractsCommands(program);
initializeConfigCommands(program);
initializeUpdateCommands(program)
program.parse(process.argv);
}

Expand Down
1 change: 0 additions & 1 deletion src/lib/interfaces/ISimulatorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export interface ISimulatorService {
getComposeOptions(): string;
checkInstallRequirements(): Promise<Record<string, boolean>>;
checkVersionRequirements(): Promise<Record<string, string>>;
pullOllamaModel(): Promise<boolean>;
runSimulator(): Promise<{stdout: string; stderr: string}>;
waitForSimulatorToBeReady(retries?: number): Promise<WaitForSimulatorToBeReadyResultType>;
createRandomValidators(numValidators: number, llmProviders: AiProviders[]): Promise<any>;
Expand Down
33 changes: 0 additions & 33 deletions src/lib/services/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,39 +150,6 @@ export class SimulatorService implements ISimulatorService {
}
}

public async pullOllamaModel(): Promise<boolean> {
try {
const ollamaContainer = this.docker.getContainer("ollama");

// Create the exec instance
const exec = await ollamaContainer.exec({
Cmd: ["ollama", "pull", "llama3"],
AttachStdout: true,
AttachStderr: true,
});

// Start the exec instance and attach to the stream
const stream = await exec.start({ Detach: false, Tty: false });

// Collect and log the output
stream.on("data", (chunk) => {
console.log(chunk.toString());
});

await new Promise<void>((resolve, reject) => {
stream.on("end", resolve);
stream.on("error", reject);
});

console.log("Command executed successfully");
return true;
} catch (error) {
console.error("Error executing ollama pull llama3:", error);
return false;
}
}


public runSimulator(): Promise<{stdout: string; stderr: string}> {
const commandsByPlatform = DEFAULT_RUN_SIMULATOR_COMMAND(this.location, this.getComposeOptions());
return executeCommand(commandsByPlatform);
Expand Down
21 changes: 14 additions & 7 deletions tests/actions/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import {join} from "path";
import fs from "fs";
import * as dotenv from "dotenv";
import {localnetCompatibleVersion} from "../../src/lib/config/simulator";
import { OllamaAction } from "../../src/commands/update/ollama";


vi.mock("fs");
vi.mock("dotenv");
vi.mock("../../src/commands/update/ollama")


const tempDir = mkdtempSync(join(tmpdir(), "test-initAction-"));
Expand All @@ -29,7 +31,6 @@ describe("init action", () => {
let simServgetAiProvidersOptions: ReturnType<any>;
let simServRunSimulator: ReturnType<any>;
let simServWaitForSimulator: ReturnType<any>;
let simServPullOllamaModel: ReturnType<any>;
let simServDeleteAllValidators: ReturnType<any>;
let simServCreateRandomValidators: ReturnType<any>;
let simServOpenFrontend: ReturnType<any>;
Expand All @@ -50,7 +51,6 @@ describe("init action", () => {
simServgetAiProvidersOptions = vi.spyOn(simulatorService, "getAiProvidersOptions");
simServRunSimulator = vi.spyOn(simulatorService, "runSimulator");
simServWaitForSimulator = vi.spyOn(simulatorService, "waitForSimulatorToBeReady");
simServPullOllamaModel = vi.spyOn(simulatorService, "pullOllamaModel");
simServDeleteAllValidators = vi.spyOn(simulatorService, "deleteAllValidators");
simServCreateRandomValidators = vi.spyOn(simulatorService, "createRandomValidators");
simServOpenFrontend = vi.spyOn(simulatorService, "openFrontend");
Expand Down Expand Up @@ -162,9 +162,11 @@ describe("init action", () => {
{ name: "OpenAI", value: "openai" },
{ name: "Heurist", value: "heuristai" },
]);

vi.mocked(OllamaAction.prototype.updateModel).mockResolvedValueOnce(undefined);

simServRunSimulator.mockResolvedValue(true);
simServWaitForSimulator.mockResolvedValue({ initialized: true });
simServPullOllamaModel.mockResolvedValue(true);
simServDeleteAllValidators.mockResolvedValue(true);
simServCreateRandomValidators.mockResolvedValue(true);
simServOpenFrontend.mockResolvedValue(true);
Expand Down Expand Up @@ -192,9 +194,11 @@ describe("init action", () => {
{ name: "OpenAI", value: "openai" },
{ name: "Heurist", value: "heuristai" },
]);

vi.mocked(OllamaAction.prototype.updateModel).mockResolvedValueOnce(undefined);

simServRunSimulator.mockResolvedValue(true);
simServWaitForSimulator.mockResolvedValue({ initialized: true });
simServPullOllamaModel.mockResolvedValue(true);
simServDeleteAllValidators.mockResolvedValue(true);
simServCreateRandomValidators.mockResolvedValue(true);
simServOpenFrontend.mockResolvedValue(true);
Expand All @@ -221,9 +225,11 @@ describe("init action", () => {
{ name: "OpenAI", value: "openai" },
{ name: "Heurist", value: "heuristai" },
]);

vi.mocked(OllamaAction.prototype.updateModel).mockResolvedValueOnce(undefined);

simServRunSimulator.mockResolvedValue(true);
simServWaitForSimulator.mockResolvedValue({ initialized: true });
simServPullOllamaModel.mockResolvedValue(true);
simServDeleteAllValidators.mockResolvedValue(true);
simServResetDockerContainers.mockResolvedValue(true);
simServResetDockerImages.mockResolvedValue(true);
Expand Down Expand Up @@ -264,16 +270,17 @@ describe("init action", () => {
{ name: "Ollama", value: "ollama" },
]);

vi.mocked(OllamaAction.prototype.updateModel).mockResolvedValueOnce(undefined);

simServRunSimulator.mockResolvedValue(true);
simServWaitForSimulator.mockResolvedValue({ initialized: true });
simServPullOllamaModel.mockResolvedValue(true);
simServDeleteAllValidators.mockResolvedValue(true);
simServResetDockerContainers.mockResolvedValue(true);
simServResetDockerImages.mockResolvedValue(true);

await initAction(defaultActionOptions, simulatorService);

expect(simServPullOllamaModel).toHaveBeenCalled();
expect(OllamaAction.prototype.updateModel).toHaveBeenCalled();
});

test("logs error if checkVersionRequirements throws", async () => {
Expand Down
129 changes: 129 additions & 0 deletions tests/actions/ollama.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import {describe, test, vi, beforeEach, afterEach, expect, Mock} from "vitest";
import { OllamaAction } from "../../src/commands/update/ollama";
import Docker from "dockerode";

vi.mock("dockerode");

describe("OllamaAction", () => {
let ollamaAction: OllamaAction;
let mockGetContainer: Mock;
let mockExec: Mock;
let mockStart: Mock;
let mockStream: any;

beforeEach(() => {
vi.clearAllMocks();
ollamaAction = new OllamaAction();

mockGetContainer = vi.mocked(Docker.prototype.getContainer);
mockExec = vi.fn();
mockStart = vi.fn();

mockStream = {
on: vi.fn(),
};

mockExec.mockResolvedValue({
start: mockStart,
});

mockStart.mockResolvedValue(mockStream);

mockGetContainer.mockReturnValue({
exec: mockExec,
} as unknown as Docker.Container);

Docker.prototype.getContainer = mockGetContainer;
});

afterEach(() => {
vi.restoreAllMocks();
});

test("should update the model using 'pull'", async () => {
mockStream.on.mockImplementation((event: any, callback:any) => {
if (event === "data") callback(Buffer.from("Mocked output"));
if (event === "end") callback();
});

console.log = vi.fn();

await ollamaAction.updateModel("mocked_model");

expect(mockGetContainer).toHaveBeenCalledWith("ollama");
expect(mockExec).toHaveBeenCalledWith({
Cmd: ["ollama", "pull", "mocked_model"],
AttachStdout: true,
AttachStderr: true,
});
expect(mockStart).toHaveBeenCalledWith({ Detach: false, Tty: false });
expect(mockStream.on).toHaveBeenCalledWith("data", expect.any(Function));
expect(mockStream.on).toHaveBeenCalledWith("end", expect.any(Function));
expect(console.log).toHaveBeenCalledWith("Mocked output");
expect(console.log).toHaveBeenCalledWith('Model "mocked_model" updated successfully');
});

test("should remove the model using 'rm'", async () => {
mockStream.on.mockImplementation((event:any, callback:any) => {
if (event === "data") callback(Buffer.from("Mocked output"));
if (event === "end") callback();
});

console.log = vi.fn();

await ollamaAction.removeModel("mocked_model");

expect(mockGetContainer).toHaveBeenCalledWith("ollama");
expect(mockExec).toHaveBeenCalledWith({
Cmd: ["ollama", "rm", "mocked_model"],
AttachStdout: true,
AttachStderr: true,
});
expect(mockStart).toHaveBeenCalledWith({ Detach: false, Tty: false });
expect(mockStream.on).toHaveBeenCalledWith("data", expect.any(Function));
expect(mockStream.on).toHaveBeenCalledWith("end", expect.any(Function));
expect(console.log).toHaveBeenCalledWith("Mocked output");
expect(console.log).toHaveBeenCalledWith('Model "mocked_model" removed successfully');
});

test("should log an error if an exception occurs during 'pull'", async () => {
const error = new Error("Mocked error");
mockGetContainer.mockReturnValueOnce(
{
exec: () => {
throw new Error("Mocked error");
}
}
);
console.error = vi.fn();

await ollamaAction.updateModel("mocked_model");

expect(mockGetContainer).toHaveBeenCalledWith("ollama");
expect(console.error).toHaveBeenCalledWith(
'Error executing command "pull" on model "mocked_model":',
error
);
});

test("should log an error if an exception occurs during 'rm'", async () => {
const error = new Error("Mocked error");
mockGetContainer.mockReturnValueOnce(
{
exec: () => {
throw new Error("Mocked error");
}
}
);

console.error = vi.fn();

await ollamaAction.removeModel("mocked_model");

expect(mockGetContainer).toHaveBeenCalledWith("ollama");
expect(console.error).toHaveBeenCalledWith(
'Error executing command "rm" on model "mocked_model":',
error
);
});
});
Loading

0 comments on commit e48e31e

Please sign in to comment.