Skip to content

Commit 8ab1d9f

Browse files
Merge branch 'feature/management-api'
2 parents 75b8b19 + 3f6e2a6 commit 8ab1d9f

78 files changed

Lines changed: 5829 additions & 27 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/Umbraco.Ai.Web.StaticAssets/wwwroot/umbraco-package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "Umbraco.Ai",
33
"name": "Umbraco Ai",
4-
"version": "17.0.0--preview.27.ge460d0a",
4+
"version": "17.0.0--preview.40.g75b8b19",
55
"allowTelemetry": true,
66
"extensions": [
77
{

src/Umbraco.Ai.Web/Api/Constants.cs

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
namespace Umbraco.Ai.Cms.Api.Management.Api;
1+
namespace Umbraco.Ai.Web.Api;
22

33
/// <summary>
44
/// Defines constants for the Umbraco AI Management API.
55
/// </summary>
66
public class Constants
77
{
88
/// <summary>
9-
/// Defines constants for the Forms Management API.
9+
/// Defines constants for the AI Management API.
1010
/// </summary>
1111
public static class ManagementApi
1212
{
1313
/// <summary>
1414
/// The API root path.
1515
/// </summary>
16-
public const string RootPath = "/umbraco/ai/management/api";
16+
public const string BackofficePath = "/ai/management/api";
1717

1818
/// <summary>
1919
/// The API title.
@@ -30,5 +30,96 @@ public static class ManagementApi
3030
/// The namespace prefix for AI Management API.
3131
/// </summary>
3232
public const string ApiNamespacePrefix = "Umbraco.Ai.Web.Api.Management";
33+
34+
/// <summary>
35+
/// The name of the API group for version 1.0.
36+
/// </summary>
37+
public const string GroupNameV1 = "1.0";
38+
39+
/// <summary>
40+
/// Defines constants for different feature areas within the Management API.
41+
/// </summary>
42+
public static class Feature
43+
{
44+
/// <summary>
45+
/// Defines constants for Connection features.
46+
/// </summary>
47+
public static class Connection
48+
{
49+
/// <summary>
50+
/// The route segment for Connection features.
51+
/// </summary>
52+
public const string RouteSegment = "connections";
53+
54+
/// <summary>
55+
/// The Swagger group name for Connection features.
56+
/// </summary>
57+
public const string GroupName = "Connections";
58+
}
59+
60+
/// <summary>
61+
/// Defines constants for Profile features.
62+
/// </summary>
63+
public static class Profile
64+
{
65+
/// <summary>
66+
/// The route segment for Profile features.
67+
/// </summary>
68+
public const string RouteSegment = "profiles";
69+
70+
/// <summary>
71+
/// The Swagger group name for Profile features.
72+
/// </summary>
73+
public const string GroupName = "Profiles";
74+
}
75+
76+
/// <summary>
77+
/// Defines constants for Provider features.
78+
/// </summary>
79+
public static class Provider
80+
{
81+
/// <summary>
82+
/// The route segment for Provider features.
83+
/// </summary>
84+
public const string RouteSegment = "providers";
85+
86+
/// <summary>
87+
/// The Swagger group name for Provider features.
88+
/// </summary>
89+
public const string GroupName = "Providers";
90+
}
91+
92+
/// <summary>
93+
/// Defines constants for Embedding features.
94+
/// </summary>
95+
public static class Embedding
96+
{
97+
/// <summary>
98+
/// The route segment for Embedding features.
99+
/// </summary>
100+
public const string RouteSegment = "embedding";
101+
102+
/// <summary>
103+
/// The Swagger group name for Embedding features.
104+
/// </summary>
105+
public const string GroupName = "Embedding";
106+
}
107+
108+
/// <summary>
109+
/// Defines constants for Chat features.
110+
/// </summary>
111+
public static class Chat
112+
{
113+
/// <summary>
114+
/// The route segment for Chat features.
115+
/// </summary>
116+
public const string RouteSegment = "chat";
117+
118+
/// <summary>
119+
/// The Swagger group name for Chat features.
120+
/// </summary>
121+
public const string GroupName = "Chat";
122+
}
123+
}
33124
}
34125
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using Umbraco.Ai.Web.Api;
3+
using Umbraco.Ai.Web.Api.Management.Common.Controllers;
4+
using Umbraco.Ai.Web.Api.Management.Common.Routing;
5+
6+
namespace Umbraco.Ai.Web.Api.Management.Chat.Controllers;
7+
8+
/// <summary>
9+
/// Base controller for Chat management API endpoints.
10+
/// </summary>
11+
[ApiExplorerSettings(GroupName = Constants.ManagementApi.Feature.Chat.GroupName)]
12+
[UmbracoAiVersionedManagementApiRoute(Constants.ManagementApi.Feature.Chat.RouteSegment)]
13+
public abstract class ChatControllerBase : UmbracoAiManagementControllerBase
14+
{
15+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using Asp.Versioning;
2+
using Microsoft.AspNetCore.Authorization;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.Extensions.AI;
6+
using Umbraco.Ai.Core.Services;
7+
using Umbraco.Ai.Web.Api.Management.Chat.Models;
8+
using Umbraco.Cms.Core.Mapping;
9+
using Umbraco.Cms.Web.Common.Authorization;
10+
11+
namespace Umbraco.Ai.Web.Api.Management.Chat.Controllers;
12+
13+
/// <summary>
14+
/// Controller for non-streaming chat completion.
15+
/// </summary>
16+
[ApiVersion("1.0")]
17+
[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)]
18+
public class CompleteChatController : ChatControllerBase
19+
{
20+
private readonly IAiChatService _chatService;
21+
private readonly IUmbracoMapper _umbracoMapper;
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="CompleteChatController"/> class.
25+
/// </summary>
26+
public CompleteChatController(IAiChatService chatService, IUmbracoMapper umbracoMapper)
27+
{
28+
_chatService = chatService;
29+
_umbracoMapper = umbracoMapper;
30+
}
31+
32+
/// <summary>
33+
/// Complete a chat conversation (non-streaming).
34+
/// </summary>
35+
/// <param name="requestModel">The chat request.</param>
36+
/// <param name="cancellationToken">Cancellation token.</param>
37+
/// <returns>The chat completion response.</returns>
38+
[HttpPost("complete")]
39+
[MapToApiVersion("1.0")]
40+
[ProducesResponseType(typeof(ChatResponseModel), StatusCodes.Status200OK)]
41+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
42+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
43+
public async Task<IActionResult> Complete(
44+
ChatRequestModel requestModel,
45+
CancellationToken cancellationToken = default)
46+
{
47+
try
48+
{
49+
// Convert request messages to ChatMessage list
50+
var messages = _umbracoMapper.MapEnumerable<ChatMessageModel, ChatMessage>(requestModel.Messages).ToList();
51+
52+
// Get chat response
53+
var response = requestModel.ProfileId.HasValue
54+
? await _chatService.GetResponseAsync(
55+
requestModel.ProfileId.Value,
56+
messages,
57+
cancellationToken: cancellationToken)
58+
: await _chatService.GetResponseAsync(
59+
messages,
60+
cancellationToken: cancellationToken);
61+
62+
return Ok(_umbracoMapper.Map<ChatResponseModel>(response));
63+
}
64+
catch (InvalidOperationException ex) when (ex.Message.Contains("not found"))
65+
{
66+
return ProfileNotFound();
67+
}
68+
catch (InvalidOperationException ex)
69+
{
70+
return BadRequest(new ProblemDetails
71+
{
72+
Title = "Chat completion failed",
73+
Detail = ex.Message,
74+
Status = StatusCodes.Status400BadRequest
75+
});
76+
}
77+
}
78+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using System.Text.Json;
2+
using Asp.Versioning;
3+
using Microsoft.AspNetCore.Authorization;
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Mvc;
6+
using Microsoft.Extensions.AI;
7+
using Umbraco.Ai.Core.Services;
8+
using Umbraco.Ai.Web.Api.Management.Chat.Models;
9+
using Umbraco.Cms.Core.Mapping;
10+
using Umbraco.Cms.Web.Common.Authorization;
11+
12+
namespace Umbraco.Ai.Web.Api.Management.Chat.Controllers;
13+
14+
/// <summary>
15+
/// Controller for streaming chat completion using Server-Sent Events (SSE).
16+
/// </summary>
17+
[ApiVersion("1.0")]
18+
[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)]
19+
public class StreamChatController : ChatControllerBase
20+
{
21+
private readonly IAiChatService _chatService;
22+
private readonly IUmbracoMapper _umbracoMapper;
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="StreamChatController"/> class.
26+
/// </summary>
27+
public StreamChatController(IAiChatService chatService, IUmbracoMapper umbracoMapper)
28+
{
29+
_chatService = chatService;
30+
_umbracoMapper = umbracoMapper;
31+
}
32+
33+
/// <summary>
34+
/// Complete a chat conversation with streaming response (SSE).
35+
/// </summary>
36+
/// <param name="requestModel">The chat request.</param>
37+
/// <param name="cancellationToken">Cancellation token.</param>
38+
/// <returns>A stream of chat completion chunks.</returns>
39+
[HttpPost("stream")]
40+
[MapToApiVersion("1.0")]
41+
[Produces("text/event-stream")]
42+
[ProducesResponseType(StatusCodes.Status200OK)]
43+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
44+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
45+
public async Task Stream(
46+
ChatRequestModel requestModel,
47+
CancellationToken cancellationToken = default)
48+
{
49+
Response.ContentType = "text/event-stream";
50+
Response.Headers.CacheControl = "no-cache";
51+
Response.Headers.Connection = "keep-alive";
52+
53+
try
54+
{
55+
// Convert request messages to ChatMessage list
56+
var messages = _umbracoMapper.MapEnumerable<ChatMessageModel, ChatMessage>(requestModel.Messages).ToList();
57+
58+
// Get streaming chat response
59+
var stream = requestModel.ProfileId.HasValue
60+
? _chatService.GetStreamingResponseAsync(
61+
requestModel.ProfileId.Value,
62+
messages,
63+
cancellationToken: cancellationToken)
64+
: _chatService.GetStreamingResponseAsync(
65+
messages,
66+
cancellationToken: cancellationToken);
67+
68+
await foreach (var update in stream.WithCancellation(cancellationToken))
69+
{
70+
var chunk = _umbracoMapper.Map<ChatStreamChunkModel>(update);
71+
72+
var json = JsonSerializer.Serialize(chunk);
73+
await Response.WriteAsync($"data: {json}\n\n", cancellationToken);
74+
await Response.Body.FlushAsync(cancellationToken);
75+
}
76+
77+
// Send final event
78+
await Response.WriteAsync("data: [DONE]\n\n", cancellationToken);
79+
await Response.Body.FlushAsync(cancellationToken);
80+
}
81+
catch (InvalidOperationException ex) when (ex.Message.Contains("not found"))
82+
{
83+
Response.StatusCode = StatusCodes.Status404NotFound;
84+
var error = JsonSerializer.Serialize(new { error = "Profile not found", detail = ex.Message });
85+
await Response.WriteAsync($"data: {error}\n\n", cancellationToken);
86+
}
87+
catch (Exception ex)
88+
{
89+
Response.StatusCode = StatusCodes.Status400BadRequest;
90+
var error = JsonSerializer.Serialize(new { error = "Chat streaming failed", detail = ex.Message });
91+
await Response.WriteAsync($"data: {error}\n\n", cancellationToken);
92+
}
93+
}
94+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using Microsoft.Extensions.AI;
2+
using Umbraco.Ai.Web.Api.Management.Chat.Models;
3+
using Umbraco.Cms.Core.Mapping;
4+
5+
namespace Umbraco.Ai.Web.Api.Management.Chat.Mapping;
6+
7+
/// <summary>
8+
/// Map definitions for Chat models.
9+
/// </summary>
10+
public class ChatMapDefinition : IMapDefinition
11+
{
12+
/// <inheritdoc />
13+
public void DefineMaps(IUmbracoMapper mapper)
14+
{
15+
mapper.Define<ChatMessageModel, ChatMessage>(Map);
16+
mapper.Define<ChatResponse, ChatResponseModel>((_, _) => new ChatResponseModel(), Map);
17+
mapper.Define<ChatResponseUpdate, ChatStreamChunkModel>((_, _) => new ChatStreamChunkModel(), Map);
18+
mapper.Define<UsageDetails, ChatUsageModel>((_, _) => new ChatUsageModel(), Map);
19+
}
20+
21+
// Umbraco.Code.MapAll
22+
private static ChatMessage Map(ChatMessageModel source, MapperContext context)
23+
{
24+
var role = source.Role.ToLowerInvariant() switch
25+
{
26+
"system" => ChatRole.System,
27+
"user" => ChatRole.User,
28+
"assistant" => ChatRole.Assistant,
29+
_ => ChatRole.User
30+
};
31+
32+
return new ChatMessage(role, source.Content);
33+
}
34+
35+
// Umbraco.Code.MapAll
36+
private static void Map(ChatResponse source, ChatResponseModel target, MapperContext context)
37+
{
38+
target.Message = new ChatMessageModel
39+
{
40+
Role = "assistant",
41+
Content = source.Text ?? string.Empty
42+
};
43+
target.FinishReason = source.FinishReason?.ToString();
44+
target.Usage = source.Usage is not null ? context.Map<ChatUsageModel>(source.Usage) : null;
45+
}
46+
47+
// Umbraco.Code.MapAll
48+
private static void Map(ChatResponseUpdate source, ChatStreamChunkModel target, MapperContext context)
49+
{
50+
target.Content = source.Text;
51+
target.FinishReason = source.FinishReason?.ToString();
52+
target.IsComplete = source.FinishReason is not null;
53+
}
54+
55+
// Umbraco.Code.MapAll
56+
private static void Map(UsageDetails source, ChatUsageModel target, MapperContext context)
57+
{
58+
target.InputTokens = source.InputTokenCount;
59+
target.OutputTokens = source.OutputTokenCount;
60+
target.TotalTokens = source.TotalTokenCount;
61+
}
62+
}

0 commit comments

Comments
 (0)