Skip to content

Commit

Permalink
Merge pull request #16 from pieces-app/feat/new-port-logic
Browse files Browse the repository at this point in the history
Add support for the new port logic in Pieces OS 11
  • Loading branch information
jimbobbennett authored Dec 19, 2024
2 parents 23abbfc + 596a23b commit 23b27cc
Showing 1 changed file with 181 additions and 57 deletions.
238 changes: 181 additions & 57 deletions src/Client/PiecesClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
/// </summary>
public class PiecesClient : IPiecesClient, IDisposable
{
private readonly PiecesApis piecesApis;
private PiecesApis? piecesApis;

private Application? application;

private readonly WebSocketBackgroundClient<QGPTStreamOutput> qgptWebSocket;
private WebSocketBackgroundClient<QGPTStreamOutput>? qgptWebSocket;
private readonly Task webSocketTask;
private readonly ILogger? logger;
private IPiecesCopilot? copilot;
Expand All @@ -31,68 +31,63 @@ public class PiecesClient : IPiecesClient, IDisposable
/// <param name="baseUrl">The URL of your Pieces OS instance. This only needs to be passed if you are not connecting to a local Pieces OS instance.</param>
public PiecesClient(ILogger? logger = null, string? baseUrl = null, string applicationVersion = "0.0.1")
{
var os = PlatformEnum.UNKNOWN;

if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
os = PlatformEnum.LINUX;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
os = PlatformEnum.WINDOWS;
}
else if (!RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD))
{
os = PlatformEnum.MACOS;
}
else
webSocketTask = Task.Run(async () =>
{
throw new PiecesClientException("OS not supported");
}

baseUrl ??= os == PlatformEnum.LINUX
? "http://localhost:5323"
: "http://localhost:1000";
// Get the platform we are running on, Windows, macOS, or Linux
var platform = GetPlatform();

var webSocketBaseUrl = baseUrl.ToLower().Replace("http://", "ws://").Replace("https://", "ws://");
// Get the base URL if it is not provided
baseUrl ??= await GetBaseUrlAsync(platform).ConfigureAwait(false);

var apiClient = new ApiClient(baseUrl);
var configuration = new GlobalConfiguration(GlobalConfiguration.Instance.DefaultHeaders,
GlobalConfiguration.Instance.ApiKey,
GlobalConfiguration.Instance.ApiKeyPrefix,
baseUrl);
// Test the base URL to ensure we can connect
var wellKnown = new WellKnownApi(baseUrl);
try
{
// The well known API returns null if it can't connect, otherwise returns a string
var health = await wellKnown.GetWellKnownHealthAsync().ConfigureAwait(false) ?? throw new PiecesClientException();
logger?.LogInformation("{health}", health);
}
catch
{
throw new PiecesClientException("Cannot connect to PiecesOS, make sure it is running");
}

piecesApis = new PiecesApis
{
ApiClient = apiClient,
AnchorsApi = new AnchorsApi(apiClient, apiClient, configuration),
AssetApi = new AssetApi(apiClient, apiClient, configuration),
AssetsApi = new AssetsApi(apiClient, apiClient, configuration),
ConnectorApi = new ConnectorApi(apiClient, apiClient, configuration),
ConversationApi = new ConversationApi(apiClient, apiClient, configuration),
ConversationsApi = new ConversationsApi(apiClient, apiClient, configuration),
ModelApi = new ModelApi(apiClient, apiClient, configuration),
ModelsApi = new ModelsApi(apiClient, apiClient, configuration),
QGPTApi = new QGPTApi(apiClient, apiClient, configuration),
RangesApi = new RangesApi(apiClient, apiClient, configuration),
WellKnownApi = new WellKnownApi(apiClient, apiClient, configuration)
};
var webSocketBaseUrl = baseUrl.ToLower().Replace("http://", "ws://").Replace("https://", "ws://");

qgptWebSocket = new WebSocketBackgroundClient<QGPTStreamOutput>();
var qgptUrlBuilder = new UriBuilder(webSocketBaseUrl)
{
Path = "qgpt/stream"
};
var apiClient = new ApiClient(baseUrl);
var configuration = new GlobalConfiguration(GlobalConfiguration.Instance.DefaultHeaders,
GlobalConfiguration.Instance.ApiKey,
GlobalConfiguration.Instance.ApiKeyPrefix,
baseUrl);

webSocketTask = Task.Run(async () =>
{
piecesApis = new PiecesApis
{
ApiClient = apiClient,
AnchorsApi = new AnchorsApi(apiClient, apiClient, configuration),
AssetApi = new AssetApi(apiClient, apiClient, configuration),
AssetsApi = new AssetsApi(apiClient, apiClient, configuration),
ConnectorApi = new ConnectorApi(apiClient, apiClient, configuration),
ConversationApi = new ConversationApi(apiClient, apiClient, configuration),
ConversationsApi = new ConversationsApi(apiClient, apiClient, configuration),
ModelApi = new ModelApi(apiClient, apiClient, configuration),
ModelsApi = new ModelsApi(apiClient, apiClient, configuration),
QGPTApi = new QGPTApi(apiClient, apiClient, configuration),
RangesApi = new RangesApi(apiClient, apiClient, configuration),
WellKnownApi = new WellKnownApi(apiClient, apiClient, configuration)
};

qgptWebSocket = new WebSocketBackgroundClient<QGPTStreamOutput>();
var qgptUrlBuilder = new UriBuilder(webSocketBaseUrl)
{
Path = "qgpt/stream"
};
var connectTask = Task.Run(async () =>
{
logger?.LogInformation("Connecting to Pieces OS...");

var application = new SeededTrackedApplication(
name: ApplicationNameEnum.OPENSOURCE,
platform: os,
platform: platform,
varVersion: applicationVersion
);
var seededConnector = new SeededConnectorConnection(application: application);
Expand All @@ -112,14 +107,143 @@ public PiecesClient(ILogger? logger = null, string? baseUrl = null, string appli

// Get all the models to pick a default - choose GPT-4o if it is available
var models = piecesApis.ModelsApi.ModelsSnapshot().Iterable;
var defaultModel = models.FirstOrDefault(x => x.Name.Contains("GPT-4o Chat", StringComparison.OrdinalIgnoreCase));
var defaultModel = models.FirstOrDefault(x => x.Name.Contains("GPT-4o Chat", StringComparison.OrdinalIgnoreCase)) ?? models.First(x => x.Cloud);

copilot = new PiecesCopilot(logger, defaultModel, application!, qgptWebSocket, piecesApis);
assets = new PiecesAssets(logger, application!, new AssetApi(apiClient, apiClient, configuration), new AssetsApi(apiClient, apiClient, configuration));
});
this.logger = logger;
}

private static PlatformEnum GetPlatform()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return PlatformEnum.LINUX;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return PlatformEnum.WINDOWS;
}
else if (!RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD))
{
return PlatformEnum.MACOS;
}

throw new PiecesClientException("OS not supported");
}

/// <summary>
/// Gets the base URL.
///
/// Pieces up to 10.1.15 is on port 1000 for Windows/macOS, and 5323 for Linux.
/// After this version, it is a port between 39300 and 39333.
///
/// To detect this port, the steps are:
/// Look at the contents of the .port.txt config file,
/// On Windows/Linux this is at ~/Documents/com.pieces.os/production/Config/.port.txt
/// On macOS, this is at ~/Library/com.pieces.os/production/Config/.port.txt
/// Otherwise, poll the port range using the WellKnownApi. This returns null if the URL/port is invalid,
/// otherwise it returns a string of OK and a GUID.
/// </summary>
/// <returns></returns>
private static async Task<string> GetBaseUrlAsync(PlatformEnum platform)
{
// Get the port from the .port.txt file
try
{
var configPort = await GetPortFromConfigAsync(platform).ConfigureAwait(false);
return $"http://localhost:{configPort}";
}
catch
{
// Either the port file is missing, or invalid, so time to poll the ports
foreach(var rangedPort in Enumerable.Range(39300, 34))
{
var wellKnown = new WellKnownApi($"http://localhost:{rangedPort}");
try
{
var health = await wellKnown.GetWellKnownHealthAsync().ConfigureAwait(false);

if (health is not null)
{
// If we get here, the port is good
return $"http://localhost:{rangedPort}";
}
}
catch
{

}
}
}

// No luck with the Pieces OS 11 port, so fall back to Pieces 10
var port = platform switch
{
PlatformEnum.LINUX => 5323,
_ => 1000
};

try
{
var wellKnown = new WellKnownApi($"http://localhost:{port}");
var health = await wellKnown.GetWellKnownHealthAsync().ConfigureAwait(false);

if (health is not null)
{
// If we get here, the port is good
return $"http://localhost:{port}";
}
}
catch
{

}

throw new PiecesClientException("Cannot connect to PiecesOS, make sure it is running");
}

private static async Task<int> GetPortFromConfigAsync(PlatformEnum platform)
{
try
{
var configPath = GetConfigFilePath(platform);
if (File.Exists(configPath))
{
var portString = (await File.ReadAllTextAsync(configPath).ConfigureAwait(false)).Trim();
return int.Parse(portString);
}
else
{
throw new FileNotFoundException($"Port configuration file not found at: {configPath}");
}
}
catch (Exception ex)
{
throw new Exception($"Error reading port configuration: {ex.Message}");
}
}

private static string GetConfigFilePath(PlatformEnum platform)
{
string userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
string relativePath;

if (platform == PlatformEnum.MACOS)
{
// macOS path
relativePath = Path.Combine("Library", "com.pieces.os", "production", "Config", ".port.txt");
}
else
{
// Windows and Linux path
relativePath = Path.Combine("Documents", "com.pieces.os", "production", "Config", ".port.txt");
}

return Path.Combine(userHome, relativePath);
}

/// <summary>
/// Gets the first model that contains the given name.
/// If no model matches, the first is returned, unless <see cref="throwIfNotFound"/>
Expand Down Expand Up @@ -160,7 +284,7 @@ public async Task<Model> GetModelByNameAsync(string modelName, bool throwIfNotFo
public void Dispose()
{
// Stop the WebSockets asynchronously and wait for completion
qgptWebSocket.StopAsync().Wait();
qgptWebSocket?.StopAsync().Wait();

// Suppress finalization for this object, as we've manually disposed of resources
GC.SuppressFinalize(this);
Expand All @@ -173,7 +297,7 @@ public void Dispose()
public async Task<IEnumerable<Model>> GetModelsAsync()
{
await EnsureConnected().ConfigureAwait(false);
return piecesApis.ModelsApi.ModelsSnapshot().Iterable;
return piecesApis!.ModelsApi.ModelsSnapshot().Iterable;
}

/// <summary>
Expand Down Expand Up @@ -201,7 +325,7 @@ public async Task<Model> DownloadModelAsync(Model model, CancellationToken cance
return model;
}

var downloadedModel = await piecesApis.ModelApi.ModelSpecificModelDownloadAsync(model.Id, cancellationToken: cancellationToken).ConfigureAwait(false);
var downloadedModel = await piecesApis!.ModelApi.ModelSpecificModelDownloadAsync(model.Id, cancellationToken: cancellationToken).ConfigureAwait(false);
if (downloadedModel.Downloaded)
{
logger?.LogInformation("Model with id: {id} already downloaded", model.Id);
Expand Down Expand Up @@ -260,7 +384,7 @@ public async Task<Model> DownloadModelAsync(string modelName, CancellationToken
public async Task<string> GetVersionAsync(CancellationToken cancellationToken = default)
{
await EnsureConnected().ConfigureAwait(false);
return await piecesApis.WellKnownApi.GetWellKnownVersionAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
return await piecesApis!.WellKnownApi.GetWellKnownVersionAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
}

/// <summary>
Expand Down

0 comments on commit 23b27cc

Please sign in to comment.