Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement conversation context and streaming with OllamaSharp #310

Merged
merged 18 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
24b9e88
Implement settings file for Ollama agent
kborowinski Nov 19, 2024
f5c0ee4
Merge branch 'main' into ollama-settings
kborowinski Nov 19, 2024
cdcc98e
Update readme and links to the documentation; Handle http exception d…
kborowinski Nov 19, 2024
c0f8389
Implement config file watcher to reload settings on change
kborowinski Nov 19, 2024
0899ddb
Do not force user to specify complete endpoint URL, just IP address a…
kborowinski Nov 19, 2024
bcc5d1e
Change json to config so comments are not rendered on GitHub in red
kborowinski Nov 19, 2024
9c358b8
Revert to json and update the comments in the config file
kborowinski Nov 19, 2024
336a3c3
Merge branch 'PowerShell:main' into ollama-settings
kborowinski Nov 19, 2024
9b57754
Add model and endpoint self check; Change warnings to errors; Fix pos…
kborowinski Nov 20, 2024
d9d6fac
Switch to OllamaSharp nuget package to simplify agent code and suppor…
kborowinski Nov 21, 2024
464d633
Add context support
kborowinski Nov 21, 2024
fe68d94
Reset context on refresh
kborowinski Nov 22, 2024
fd95d98
Implement changes as suggested in the code review
kborowinski Nov 23, 2024
ee76005
Add missing new line at the end of file
kborowinski Nov 23, 2024
a217daa
Merge branch 'main' into ollama-sharp
kborowinski Nov 23, 2024
cdbee04
Check if ollama is running only for local endpoint
kborowinski Nov 24, 2024
29b4c49
Update description and readme that ollama is also supported remotely;…
kborowinski Nov 24, 2024
b798172
Minor updates
daxian-dbw Dec 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
kborowinski marked this conversation as resolved.
Show resolved Hide resolved

<!-- Disable deps.json generation -->
<GenerateDependencyFile>false</GenerateDependencyFile>
Expand All @@ -16,6 +17,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="OllamaSharp" Version="4.0.7" />
</ItemGroup>

<ItemGroup>
kborowinski marked this conversation as resolved.
Show resolved Hide resolved
<ProjectReference Include="..\..\AIShell.Abstraction\AIShell.Abstraction.csproj">
<!-- Disable copying AIShell.Abstraction.dll to output folder -->
<Private>false</Private>
Expand Down
249 changes: 226 additions & 23 deletions shell/agents/AIShell.Ollama.Agent/OllamaAgent.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using AIShell.Abstraction;
using OllamaSharp;
using OllamaSharp.Models;

namespace AIShell.Ollama.Agent;

public sealed class OllamaAgent : ILLMAgent
{
private bool _reloadSettings;
private bool _isDisposed;
private string _configRoot;
private Settings _settings;
private OllamaApiClient _client;
private GenerateRequest _request;
private FileSystemWatcher _watcher;

/// <summary>
/// The name of setting file
/// </summary>
private const string SettingFileName = "ollama.config.json";

/// <summary>
/// Gets the settings.
/// </summary>
internal Settings Settings => _settings;

/// <summary>
/// The name of the agent
/// </summary>
Expand All @@ -13,7 +35,7 @@ public sealed class OllamaAgent : ILLMAgent
/// <summary>
/// The description of the agent to be shown at start up
/// </summary>
public string Description => "This is an AI assistant to interact with a language model running locally by utilizing the Ollama CLI tool. Be sure to follow all prerequisites in aka.ms/aish/ollama";
public string Description => "This is an AI assistant to interact with a language model running locally by utilizing the Ollama CLI tool. Be sure to follow all prerequisites in https://github.com/PowerShell/AIShell/tree/main/shell/agents/AIShell.Ollama.Agent";

/// <summary>
/// This is the company added to /like and /dislike verbiage for who the telemetry helps.
Expand All @@ -30,19 +52,25 @@ public sealed class OllamaAgent : ILLMAgent
/// <summary>
/// These are any optional legal/additional information links you want to provide at start up
/// </summary>
public Dictionary<string, string> LegalLinks { private set; get; }

/// <summary>
/// This is the chat service to call the API from
/// </summary>
private OllamaChatService _chatService;
public Dictionary<string, string> LegalLinks { private set; get; } = new(StringComparer.OrdinalIgnoreCase)
{
["Ollama Docs"] = "https://github.com/ollama/ollama",
["Prerequisites"] = "https://github.com/PowerShell/AIShell/tree/main/shell/agents/AIShell.Ollama.Agent"
};

/// <summary>
/// Dispose method to clean up the unmanaged resource of the chatService
/// </summary>
public void Dispose()
{
_chatService?.Dispose();
if (_isDisposed)
{
return;
}

GC.SuppressFinalize(this);
_watcher.Dispose();
_isDisposed = true;
}

/// <summary>
Expand All @@ -51,13 +79,30 @@ public void Dispose()
/// <param name="config">Agent configuration for any configuration file and other settings</param>
public void Initialize(AgentConfig config)
{
_chatService = new OllamaChatService();
_configRoot = config.ConfigurationRoot;

SettingFile = Path.Combine(_configRoot, SettingFileName);
_settings = ReadSettings();

if (_settings is null)
{
// Create the setting file with examples to serve as a template for user to update.
NewExampleSettingFile();
_settings = ReadSettings();
}

_request = new GenerateRequest()
{
Model = _settings.Model,
Stream = _settings.Stream
};
kborowinski marked this conversation as resolved.
Show resolved Hide resolved

LegalLinks = new(StringComparer.OrdinalIgnoreCase)
_watcher = new FileSystemWatcher(_configRoot, SettingFileName)
{
["Ollama Docs"] = "https://github.com/ollama/ollama",
["Prerequisites"] = "https://aka.ms/ollama/readme"
NotifyFilter = NotifyFilters.LastWrite,
EnableRaisingEvents = true,
};
_watcher.Changed += OnSettingFileChange;
}

/// <summary>
Expand All @@ -68,7 +113,7 @@ public void Initialize(AgentConfig config)
/// <summary>
/// Gets the path to the setting file of the agent.
/// </summary>
public string SettingFile { private set; get; } = null;
public string SettingFile { private set; get; }

/// <summary>
/// Gets a value indicating whether the agent accepts a specific user action feedback.
Expand All @@ -87,7 +132,19 @@ public void OnUserAction(UserActionPayload actionPayload) {}
/// Refresh the current chat by starting a new chat session.
/// This method allows an agent to reset chat states, interact with user for authentication, print welcome message, and more.
/// </summary>
public Task RefreshChatAsync(IShell shell, bool force) => Task.CompletedTask;
public Task RefreshChatAsync(IShell shell, bool force)
{
if (force)
{
// Reload the setting file if needed.
ReloadSettings();

// Reset context
_request.Context = null;
}

return Task.CompletedTask;
}

/// <summary>
/// Main chat function that takes the users input and passes it to the LLM and renders it.
Expand All @@ -103,23 +160,169 @@ public async Task<bool> ChatAsync(string input, IShell shell)
// get the cancellation token
CancellationToken token = shell.CancellationToken;

// Reload the setting file if needed.
ReloadSettings();

if (Process.GetProcessesByName("ollama").Length is 0)
{
host.RenderFullResponse("Please be sure the Ollama is installed and server is running. Check all the prerequisites in the README of this agent are met.");
host.MarkupWarningLine($"[[{Name}]]: Please be sure the Ollama is installed and server is running. Check all the prerequisites in the README of this agent are met.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
host.MarkupWarningLine($"[[{Name}]]: Please be sure the Ollama is installed and server is running. Check all the prerequisites in the README of this agent are met.");
host.WriteErrorLine($"[{Name}]: Please be sure the Ollama is installed and server is running. Check all the prerequisites in the README of this agent are met.");

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, given that the prompt will be @ollama when the agent is in use, maybe we don't need to include the [{Name}] part in the error message.

Copy link
Contributor Author

@kborowinski kborowinski Nov 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, I think we should check if ollama is running only when the Endpoint is set to localhost. This will enable support for remote ollama endpoints.

if (IsLocalHost().IsMatch(_client.Uri.Host) && Process.GetProcessesByName("ollama").Length is 0)
{
    host.WriteErrorLine("Please be sure the Ollama is installed and server is running. Check all the prerequisites in the README of this agent are met.");
    return false;
}

and

/// <summary>
/// Defines a generated regular expression to match localhost addresses
/// "localhost", "127.0.0.1" and "[::1]" with case-insensitivity.
/// </summary>
[GeneratedRegex("^(localhost|127\\.0\\.0\\.1|\\[::1\\])$", RegexOptions.IgnoreCase)]
internal partial Regex IsLocalHost();

return false;
}

// Self check settings Model and Endpoint
if (!SelfCheck(host))
{
return false;
}

ResponseData ollamaResponse = await host.RunWithSpinnerAsync(
status: "Thinking ...",
func: async context => await _chatService.GetChatResponseAsync(context, input, token)
).ConfigureAwait(false);
// Update request
_request.Prompt = input;
_request.Model = _settings.Model;
_request.Stream = _settings.Stream;

// Ollama client is created per chat with reloaded settings
_client = new OllamaApiClient(_settings.Endpoint);
kborowinski marked this conversation as resolved.
Show resolved Hide resolved

try
{
if (_request.Stream)
{
using IStreamRender streamingRender = host.NewStreamRender(token);

// Last stream response has context value
GenerateDoneResponseStream ollamaLastStream = null;

// Directly process the stream when no spinner is needed
await foreach (var ollamaStream in _client.GenerateAsync(_request, token))
kborowinski marked this conversation as resolved.
Show resolved Hide resolved
{
// Update the render
streamingRender.Refresh(ollamaStream.Response);
if (ollamaStream.Done)
{
ollamaLastStream = (GenerateDoneResponseStream)ollamaStream;
}
}

// Update request context
_request.Context = ollamaLastStream.Context;
kborowinski marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
// Build single response with spinner
var ollamaResponse = await host.RunWithSpinnerAsync(
status: "Thinking ...",
func: async () => { return await _client.GenerateAsync(_request, token).StreamToEndAsync(); }
).ConfigureAwait(false);

if (ollamaResponse is not null)
// Update request context
_request.Context = ollamaResponse.Context;

// Render the full response
host.RenderFullResponse(ollamaResponse.Response);
}
}
catch (OperationCanceledException)
{
// render the content
host.RenderFullResponse(ollamaResponse.response);
// Ignore the cancellation exception.
}

catch (HttpRequestException e)
{
host.WriteErrorLine($"[{Name}]: {e.Message}");
host.WriteErrorLine($"[{Name}]: Selected Model : \"{_settings.Model}\"");
host.WriteErrorLine($"[{Name}]: Selected Endpoint : \"{_settings.Endpoint}\"");
host.WriteErrorLine($"[{Name}]: Configuration File: \"{SettingFile}\"");
kborowinski marked this conversation as resolved.
Show resolved Hide resolved
return false;
kborowinski marked this conversation as resolved.
Show resolved Hide resolved
}

return true;
}


private void ReloadSettings()
kborowinski marked this conversation as resolved.
Show resolved Hide resolved
{
if (_reloadSettings)
{
_reloadSettings = false;
var settings = ReadSettings();
if (settings is null)
{
return;
}

_settings = settings;
}
}

private Settings ReadSettings()
{
Settings settings = null;
FileInfo file = new(SettingFile);

if (file.Exists)
{
try
{
using var stream = file.OpenRead();
var data = JsonSerializer.Deserialize(stream, SourceGenerationContext.Default.ConfigData);
settings = new Settings(data);
}
catch (Exception e)
{
throw new InvalidDataException($"Parsing settings from '{SettingFile}' failed with the following error: {e.Message}", e);
}
}

return settings;
}

private void OnSettingFileChange(object sender, FileSystemEventArgs e)
{
if (e.ChangeType is WatcherChangeTypes.Changed)
{
_reloadSettings = true;
}
}

private bool SelfCheck(IHost host)
{
var settings = new (string settingValue, string settingName)[]
{
(_settings.Model, "Model"),
(_settings.Endpoint, "Endpoint")
};

foreach (var (settingValue, settingName) in settings)
{
if (string.IsNullOrWhiteSpace(settingValue))
{
host.WriteErrorLine($"[{Name}]: {settingName} is undefined in the settings file: \"{SettingFile}\"");
kborowinski marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
}

return true;
}

private void NewExampleSettingFile()
{
string SampleContent = $$"""
{
// To use Ollama API service:
// 1. Install Ollama:
// winget install Ollama.Ollama
// 2. Start Ollama API server:
// ollama serve
// 3. Install Ollama model:
// ollama pull phi3

// Declare Ollama model
"Model": "phi3",
// Declare Ollama endpoint
"Endpoint": "http://localhost:11434",
// Enable Ollama streaming
"Stream": false
}
""";
kborowinski marked this conversation as resolved.
Show resolved Hide resolved
File.WriteAllText(SettingFile, SampleContent, Encoding.UTF8);
}
}
Loading