Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,29 @@ var substituted = substitutor.Substitute(
```

### OpenAPI conversion
End-to-end conversion using `OpenApiToUtcpConverter`:
You can register an OpenAPI spec by passing its URL as an `HttpCallTemplate` to `RegisterManualAsync`. The HTTP protocol automatically fetches and converts JSON OpenAPI documents into a UTCP manual at registration time.

Example (automatic conversion on register):
```csharp
using Utcp.Http;

var client = await UtcpClientImplementation.CreateAsync(config: new UtcpClientConfig
{
ToolRepository = new Utcp.Core.Repositories.InMemToolRepository(),
ToolSearchStrategy = new Utcp.Core.Search.TagAndDescriptionWordMatchStrategy(),
});

await client.RegisterManualAsync(new HttpCallTemplate
{
CallTemplateType = "http",
Name = "petstore",
Url = new Uri("https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.json"),
});

var tools = await client.SearchToolsAsync("pet store", 5);
```

End-to-end conversion using `OpenApiToUtcpConverter` (manual control):
```csharp
using Utcp.Http.OpenApi;
using System.Net.Http;
Expand Down
69 changes: 67 additions & 2 deletions src/Utcp.Http/HttpCommunicationProtocol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Utcp.Http;
using Utcp.Core;
using Utcp.Core.Interfaces;
using Utcp.Core.Models;
using Utcp.Http.OpenApi;

public sealed class HttpCommunicationProtocol : ICommunicationProtocol
{
Expand All @@ -17,9 +18,73 @@ public HttpCommunicationProtocol(IHttpClientFactory httpClientFactory)
this.httpClientFactory = httpClientFactory;
}

public Task<RegisterManualResult> RegisterManualAsync(UtcpClient caller, CallTemplate manualCallTemplate, CancellationToken cancellationToken = default)
public async Task<RegisterManualResult> RegisterManualAsync(UtcpClient caller, CallTemplate manualCallTemplate, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
// Auto-convert OpenAPI specs into UTCP manuals if the manual template points to an OpenAPI document
if (manualCallTemplate is HttpCallTemplate http && http.Url is not null)
{
var client = this.httpClientFactory.CreateClient("utcp");
if (http.Timeout is not null)
{
client.Timeout = http.Timeout.Value;
}

using var req = new HttpRequestMessage(HttpMethod.Get, http.Url);
if (http.Headers is not null)
{
foreach (var (k, v) in http.Headers)
{
req.Headers.TryAddWithoutValidation(k, v);
}
}

try
{
using var resp = await client.SendAsync(req, cancellationToken).ConfigureAwait(false);
resp.EnsureSuccessStatusCode();
var specText = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);

// Heuristic: treat JSON OpenAPI documents (we look for root object text)
var trimmed = specText.TrimStart();
if (trimmed.StartsWith("{"))
{
var converter = new OpenApiToUtcpConverter();
var manual = converter.FromJson(specText, http.Name ?? string.Empty);
return new RegisterManualResult
{
ManualCallTemplate = manualCallTemplate,
Manual = manual,
Success = true,
};
}
}
catch (Exception ex)
{
return new RegisterManualResult
{
ManualCallTemplate = manualCallTemplate,
Manual = new UtcpManual { Tools = Array.Empty<Tool>() },
Success = false,
Errors = new[] { ex.Message },
};
}

// Not an OpenAPI JSON document; default to no tools
return new RegisterManualResult
{
ManualCallTemplate = manualCallTemplate,
Manual = new UtcpManual { Tools = Array.Empty<Tool>() },
Success = true,
};
}

// For unsupported template types, return empty manual by default
return new RegisterManualResult
{
ManualCallTemplate = manualCallTemplate,
Manual = new UtcpManual { Tools = Array.Empty<Tool>() },
Success = true,
};
}

public Task DeregisterManualAsync(UtcpClient caller, CallTemplate manualCallTemplate, CancellationToken cancellationToken = default)
Expand Down
26 changes: 26 additions & 0 deletions tests/Utcp.Http.Tests/HttpCommunicationProtocolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,35 @@
using Utcp.Http;
using FluentAssertions;
using Xunit;
using Utcp.Http.OpenApi;

public class HttpCommunicationProtocolTests
{
[Fact]
public async Task RegisterManual_OpenApiJson_ConvertsToTools()
{
const string openApi = """
{"openapi":"3.0.0","info":{"title":"t","version":"1.0.0"},"servers":[{"url":"https://api.example.com"}],"paths":{"/status":{"get":{"operationId":"getStatus","summary":"status"}}}}
""";

var mock = new MockHttpMessageHandler();
mock.When(HttpMethod.Get, "https://api.example.com/openapi.json")
.Respond("application/json", openApi);

var httpClientFactory = new MockFactory(mock);
var protocol = new HttpCommunicationProtocol(httpClientFactory);

var template = new HttpCallTemplate
{
CallTemplateType = "http",
Name = "manual",
Url = new Uri("https://api.example.com/openapi.json"),
};

var result = await protocol.RegisterManualAsync(new DummyClient(), template);
result.Success.Should().BeTrue();
result.Manual.Tools.Should().ContainSingle(t => t.Name == "manual.getStatus");
}
[Fact]
public async Task CallTool_BasicGet_ReturnsBody()
{
Expand Down
Loading