diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTask.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTask.cs index 9563a571..332fcc63 100644 --- a/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTask.cs +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Models/PostSessionTask.cs @@ -40,19 +40,4 @@ public sealed class PostSessionTask /// Gets or sets the AI tool names available to this task during post-session processing. /// public string[] ToolNames { get; set; } = []; - - /// - /// Gets or sets the AI agent profile names available to this task during post-session processing. - /// - public string[] AgentNames { get; set; } = []; - - /// - /// Gets or sets the A2A connection identifiers available to this task during post-session processing. - /// - public string[] A2AConnectionIds { get; set; } = []; - - /// - /// Gets or sets the MCP connection identifiers available to this task during post-session processing. - /// - public string[] McpConnectionIds { get; set; } = []; } diff --git a/src/Primitives/CrestApps.Core.AI.Chat/Services/AIChatSessionCloseCycleService.cs b/src/Primitives/CrestApps.Core.AI.Chat/Services/AIChatSessionCloseCycleService.cs index ec88c679..ff771eb1 100644 --- a/src/Primitives/CrestApps.Core.AI.Chat/Services/AIChatSessionCloseCycleService.cs +++ b/src/Primitives/CrestApps.Core.AI.Chat/Services/AIChatSessionCloseCycleService.cs @@ -18,22 +18,18 @@ public sealed class AIChatSessionCloseCycleService private static readonly TimeSpan _defaultInactivityTimeout = TimeSpan.FromMinutes(30); private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(5); - private readonly IServiceProvider _serviceProvider; private readonly TimeProvider _timeProvider; private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// - /// The service provider. /// The time provider. /// The logger. public AIChatSessionCloseCycleService( - IServiceProvider serviceProvider, TimeProvider timeProvider, ILogger logger) { - _serviceProvider = serviceProvider; _timeProvider = timeProvider; _logger = logger; } @@ -41,79 +37,66 @@ public AIChatSessionCloseCycleService( /// /// Runs one AI chat session close cycle immediately. /// + /// The root service provider used to create short-lived scopes per work unit. /// The cancellation token. - public async Task RunOnceAsync(CancellationToken cancellationToken = default) + public async Task RunOnceAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken = default) { try { - var sessionStore = _serviceProvider.GetRequiredService(); - var profileManager = _serviceProvider.GetRequiredService(); - var postCloseProcessor = _serviceProvider.GetRequiredService(); - var promptStore = _serviceProvider.GetRequiredService(); - var storeCommitter = _serviceProvider.GetRequiredService(); var utcNow = _timeProvider.GetUtcNow().UtcDateTime; - var profiles = (await profileManager.GetAsync(AIProfileType.Chat, cancellationToken)).ToList(); + + List workItems; + + using (var discoveryScope = serviceProvider.CreateScope()) + { + workItems = await DiscoverWorkAsync(discoveryScope.ServiceProvider, utcNow, cancellationToken); + } + + if (workItems.Count == 0) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("AI chat session close cycle completed with no work."); + } + + return; + } + var closedCount = 0; var abandonedCount = 0; var retriedCount = 0; var recoveredCount = 0; var failedCount = 0; - foreach (var profile in profiles) + foreach (var workItem in workItems) { if (cancellationToken.IsCancellationRequested) { break; } - var (profileClosedCount, profileAbandonedCount) = await CloseInactiveSessionsAsync( - sessionStore, - promptStore, - postCloseProcessor, - profile, - utcNow, - cancellationToken); - - closedCount += profileClosedCount; - abandonedCount += profileAbandonedCount; - - var (profileRetriedCount, profileRecoveredCount, profileFailedCount) = await RetryPendingProcessingAsync( - sessionStore, - promptStore, - postCloseProcessor, - profile, - utcNow, - cancellationToken); - - retriedCount += profileRetriedCount; - recoveredCount += profileRecoveredCount; - failedCount += profileFailedCount; - } + using var processScope = serviceProvider.CreateScope(); - await storeCommitter.CommitAsync(cancellationToken); + var result = await ProcessWorkItemAsync(processScope.ServiceProvider, workItem, utcNow, cancellationToken); - if ((closedCount > 0 - || abandonedCount > 0 - || retriedCount > 0 - || recoveredCount > 0 - || failedCount > 0) - && _logger.IsEnabled(LogLevel.Information)) + closedCount += result.ClosedCount; + abandonedCount += result.AbandonedCount; + retriedCount += result.RetriedCount; + recoveredCount += result.RecoveredCount; + failedCount += result.FailedCount; + } + + if (_logger.IsEnabled(LogLevel.Information)) { _logger.LogInformation( - "AI chat session close cycle completed. Profiles={ProfileCount}, Closed={ClosedCount}, Abandoned={AbandonedCount}, Retried={RetriedCount}, Recovered={RecoveredCount}, Failed={FailedCount}.", - profiles.Count, + "AI chat session close cycle completed. Sessions={SessionCount}, Closed={ClosedCount}, Abandoned={AbandonedCount}, Retried={RetriedCount}, Recovered={RecoveredCount}, Failed={FailedCount}.", + workItems.Count, closedCount, abandonedCount, retriedCount, recoveredCount, failedCount); } - else if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug( - "AI chat session close cycle completed with no work. Profiles evaluated: {ProfileCount}.", - profiles.Count); - } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -125,145 +108,178 @@ public async Task RunOnceAsync(CancellationToken cancellationToken = default) } } - private async Task<(int ClosedCount, int AbandonedCount)> CloseInactiveSessionsAsync( - IAIChatSessionStore sessionStore, - IAIChatSessionPromptStore promptStore, - AIChatSessionPostCloseProcessor postCloseProcessor, - AIProfile profile, + private static async Task> DiscoverWorkAsync( + IServiceProvider serviceProvider, DateTime utcNow, CancellationToken cancellationToken) { - var settings = profile.GetOrCreateSettings(); - var timeout = settings?.SessionInactivityTimeoutInMinutes > 0 - ? TimeSpan.FromMinutes(settings.SessionInactivityTimeoutInMinutes) - : _defaultInactivityTimeout; - var cutoffUtc = utcNow - timeout; - var closedCount = 0; - var abandonedCount = 0; - - var inactiveSessions = await sessionStore.GetInactiveActiveSessionsAsync(profile.ItemId, cutoffUtc, cancellationToken); + var sessionStore = serviceProvider.GetRequiredService(); + var profileManager = serviceProvider.GetRequiredService(); + var postCloseProcessor = serviceProvider.GetRequiredService(); + var profiles = await profileManager.GetAsync(AIProfileType.Chat, cancellationToken); + var workItems = new List(); - foreach (var chatSession in inactiveSessions) + foreach (var profile in profiles) { if (cancellationToken.IsCancellationRequested) { break; } - var prompts = await promptStore.GetPromptsAsync(chatSession.SessionId); - chatSession.Status = DetermineInactiveSessionStatus(prompts); - chatSession.ClosedAtUtc = utcNow; + var settings = profile.GetOrCreateSettings(); + var timeout = settings?.SessionInactivityTimeoutInMinutes > 0 + ? TimeSpan.FromMinutes(settings.SessionInactivityTimeoutInMinutes) + : _defaultInactivityTimeout; + var cutoffUtc = utcNow - timeout; - if (postCloseProcessor.QueueIfNeeded(profile, chatSession)) - { - await postCloseProcessor.ProcessAsync(profile, chatSession, prompts, cancellationToken); - } - else + var inactiveSessions = await sessionStore.GetInactiveActiveSessionsAsync(profile.ItemId, cutoffUtc, cancellationToken); + + foreach (var session in inactiveSessions) { - chatSession.PostSessionProcessingStatus = PostSessionProcessingStatus.None; + workItems.Add(new SessionWorkItem(profile.ItemId, session.SessionId, SessionWorkType.Close)); } - await sessionStore.SaveAsync(chatSession, cancellationToken); + var closedSessions = await sessionStore.GetClosedSessionsAsync(profile.ItemId, cancellationToken); - if (chatSession.Status == ChatSessionStatus.Closed) + foreach (var session in closedSessions) { - closedCount++; - } - else - { - abandonedCount++; - } + if (!postCloseProcessor.NeedsProcessing(profile, session)) + { + continue; + } - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug( - "Finalized inactive session '{SessionId}' for profile '{ProfileId}' as '{Status}'. Post-processing: {NeedsProcessing}.", - chatSession.SessionId, - profile.ItemId, - chatSession.Status, - chatSession.PostSessionProcessingStatus != PostSessionProcessingStatus.None); + if (session.PostSessionProcessingAttempts >= postCloseProcessor.MaxPostCloseAttempts) + { + workItems.Add(new SessionWorkItem(profile.ItemId, session.SessionId, SessionWorkType.MarkFailed)); + + continue; + } + + if (session.PostSessionProcessingLastAttemptUtc.HasValue + && (utcNow - session.PostSessionProcessingLastAttemptUtc.Value) < _retryDelay) + { + continue; + } + + workItems.Add(new SessionWorkItem(profile.ItemId, session.SessionId, SessionWorkType.Retry)); } } - return (closedCount, abandonedCount); + return workItems; } - private async Task<(int RetriedCount, int RecoveredCount, int FailedCount)> RetryPendingProcessingAsync( - IAIChatSessionStore sessionStore, - IAIChatSessionPromptStore promptStore, - AIChatSessionPostCloseProcessor postCloseProcessor, - AIProfile profile, + private async Task ProcessWorkItemAsync( + IServiceProvider serviceProvider, + SessionWorkItem workItem, DateTime utcNow, CancellationToken cancellationToken) { - var retriedCount = 0; - var recoveredCount = 0; - var failedCount = 0; + var sessionStore = serviceProvider.GetRequiredService(); + var profileManager = serviceProvider.GetRequiredService(); + var postCloseProcessor = serviceProvider.GetRequiredService(); + var promptStore = serviceProvider.GetRequiredService(); + var storeCommitter = serviceProvider.GetRequiredService(); - var closedSessions = await sessionStore.GetClosedSessionsAsync(profile.ItemId, cancellationToken); + var profile = await profileManager.FindByIdAsync(workItem.ProfileId, cancellationToken); - foreach (var chatSession in closedSessions) + if (profile is null) { - if (cancellationToken.IsCancellationRequested) - { - break; - } + return default; + } - var originalStatus = chatSession.PostSessionProcessingStatus; - var needsQueuedProcessing = postCloseProcessor.QueueIfNeeded(profile, chatSession); + var chatSession = await sessionStore.FindByIdAsync(workItem.SessionId, cancellationToken); - if (!needsQueuedProcessing) - { - continue; - } + if (chatSession is null) + { + return default; + } - if (originalStatus != PostSessionProcessingStatus.Pending) - { - recoveredCount++; + var result = new WorkItemResult(); - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation( - "Recovered closed session '{SessionId}' for post-close processing. Previous processing status was '{PreviousStatus}'.", - chatSession.SessionId, - originalStatus); - } - } + switch (workItem.WorkType) + { + case SessionWorkType.Close: + await ProcessCloseAsync(sessionStore, promptStore, postCloseProcessor, profile, chatSession, utcNow, cancellationToken); + result.ClosedCount = chatSession.Status == ChatSessionStatus.Closed ? 1 : 0; + result.AbandonedCount = chatSession.Status == ChatSessionStatus.Abandoned ? 1 : 0; + break; - if (chatSession.PostSessionProcessingAttempts >= postCloseProcessor.MaxPostCloseAttempts) - { + case SessionWorkType.Retry: + await ProcessRetryAsync(sessionStore, promptStore, postCloseProcessor, profile, chatSession, cancellationToken); + result.RetriedCount = 1; + result.RecoveredCount = chatSession.PostSessionProcessingStatus == PostSessionProcessingStatus.Completed ? 1 : 0; + break; + + case SessionWorkType.MarkFailed: chatSession.PostSessionProcessingStatus = PostSessionProcessingStatus.Failed; await sessionStore.SaveAsync(chatSession, cancellationToken); - failedCount++; + result.FailedCount = 1; _logger.LogWarning( "Post-session processing for session '{SessionId}' failed after {MaxAttempts} attempts.", chatSession.SessionId, postCloseProcessor.MaxPostCloseAttempts); + break; + } - continue; - } + await storeCommitter.CommitAsync(cancellationToken); - if (chatSession.PostSessionProcessingLastAttemptUtc.HasValue - && (utcNow - chatSession.PostSessionProcessingLastAttemptUtc.Value) < _retryDelay) - { - continue; - } + return result; + } + + private async Task ProcessCloseAsync( + IAIChatSessionStore sessionStore, + IAIChatSessionPromptStore promptStore, + AIChatSessionPostCloseProcessor postCloseProcessor, + AIProfile profile, + AIChatSession chatSession, + DateTime utcNow, + CancellationToken cancellationToken) + { + var prompts = await promptStore.GetPromptsAsync(chatSession.SessionId); + chatSession.Status = DetermineInactiveSessionStatus(prompts); + chatSession.ClosedAtUtc = utcNow; - var prompts = await promptStore.GetPromptsAsync(chatSession.SessionId); + if (postCloseProcessor.QueueIfNeeded(profile, chatSession)) + { await postCloseProcessor.ProcessAsync(profile, chatSession, prompts, cancellationToken); - await sessionStore.SaveAsync(chatSession, cancellationToken); - retriedCount++; + } + else + { + chatSession.PostSessionProcessingStatus = PostSessionProcessingStatus.None; + } - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug( - "Processed pending post-close work for session '{SessionId}'.", - chatSession.SessionId); - } + await sessionStore.SaveAsync(chatSession, cancellationToken); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug( + "Finalized inactive session '{SessionId}' for profile '{ProfileId}' as '{Status}'. Post-processing: {NeedsProcessing}.", + chatSession.SessionId, + profile.ItemId, + chatSession.Status, + chatSession.PostSessionProcessingStatus != PostSessionProcessingStatus.None); } + } - return (retriedCount, recoveredCount, failedCount); + private async Task ProcessRetryAsync( + IAIChatSessionStore sessionStore, + IAIChatSessionPromptStore promptStore, + AIChatSessionPostCloseProcessor postCloseProcessor, + AIProfile profile, + AIChatSession chatSession, + CancellationToken cancellationToken) + { + var prompts = await promptStore.GetPromptsAsync(chatSession.SessionId); + await postCloseProcessor.ProcessAsync(profile, chatSession, prompts, cancellationToken); + await sessionStore.SaveAsync(chatSession, cancellationToken); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug( + "Processed pending post-close work for session '{SessionId}'.", + chatSession.SessionId); + } } private static ChatSessionStatus DetermineInactiveSessionStatus(IReadOnlyList prompts) @@ -274,4 +290,22 @@ private static ChatSessionStatus DetermineInactiveSessionStatus(IReadOnlyList _logger; private readonly Lock _syncLock = new(); @@ -22,12 +23,15 @@ public sealed class AIChatSessionCloseRunner /// Initializes a new instance of the class. /// /// The shared cycle service. + /// The service provider. /// The logger. public AIChatSessionCloseRunner( AIChatSessionCloseCycleService cycleService, + IServiceProvider serviceProvider, ILogger logger) { _cycleService = cycleService; + _serviceProvider = serviceProvider; _logger = logger; } @@ -104,20 +108,20 @@ public async Task StopAsync(CancellationToken cancellationToken = default) /// Runs the shared AI chat session close cycle immediately. /// /// The cancellation token. - public Task RunOnceAsync(CancellationToken cancellationToken = default) + public async Task RunOnceAsync(CancellationToken cancellationToken = default) { - return _cycleService.RunOnceAsync(cancellationToken); + await _cycleService.RunOnceAsync(_serviceProvider, cancellationToken); } private async Task RunAsync(CancellationToken stoppingToken) { - await _cycleService.RunOnceAsync(stoppingToken); + await _cycleService.RunOnceAsync(_serviceProvider, stoppingToken); using var timer = new PeriodicTimer(_interval); while (await timer.WaitForNextTickAsync(stoppingToken)) { - await _cycleService.RunOnceAsync(stoppingToken); + await _cycleService.RunOnceAsync(_serviceProvider, stoppingToken); } } } diff --git a/src/Primitives/CrestApps.Core.AI.Chat/Services/PostSessionProcessingService.cs b/src/Primitives/CrestApps.Core.AI.Chat/Services/PostSessionProcessingService.cs index 90956c0e..e4a953eb 100644 --- a/src/Primitives/CrestApps.Core.AI.Chat/Services/PostSessionProcessingService.cs +++ b/src/Primitives/CrestApps.Core.AI.Chat/Services/PostSessionProcessingService.cs @@ -4,6 +4,7 @@ using CrestApps.Core.AI.Models; using CrestApps.Core.AI.Orchestration; using CrestApps.Core.AI.Services; +using CrestApps.Core.AI.Tooling; using CrestApps.Core.Support.Json; using CrestApps.Core.Templates.Parsing; using CrestApps.Core.Templates.Services; @@ -21,7 +22,7 @@ public sealed class PostSessionProcessingService { private readonly IAIClientFactory _clientFactory; private readonly IAIDeploymentManager _deploymentManager; - private readonly IAIToolsService _toolsService; + private readonly IToolRegistry _toolRegistry; private readonly ITemplateService _aiTemplateService; private readonly ITemplateParser _markdownTemplateParser; private readonly IServiceProvider _serviceProvider; @@ -35,7 +36,7 @@ public sealed class PostSessionProcessingService /// Initializes a new instance of the class. /// /// The client factory. - /// The tools service. + /// The tool registry. /// The ai template service. /// The registered template parsers. /// The default options. @@ -45,7 +46,7 @@ public sealed class PostSessionProcessingService /// The deployment manager. public PostSessionProcessingService( IAIClientFactory clientFactory, - IAIToolsService toolsService, + IToolRegistry toolRegistry, ITemplateService aiTemplateService, IEnumerable templateParsers, DefaultAIOptions defaultOptions, @@ -56,7 +57,7 @@ public PostSessionProcessingService( { _clientFactory = clientFactory; _deploymentManager = deploymentManager; - _toolsService = toolsService; + _toolRegistry = toolRegistry; _aiTemplateService = aiTemplateService; _markdownTemplateParser = ResolveMarkdownTemplateParser(templateParsers); _serviceProvider = serviceProvider; @@ -338,9 +339,7 @@ public async Task> ProcessAsync( new(ChatRole.User, prompt), }; - var toolNames = GetConfiguredToolNames(settings.ToolNames, tasksToProcess); - - var tools = await ResolveToolsAsync(session.SessionId, toolNames); + var tools = await ResolveToolsAsync(session.SessionId, settings.ToolNames, tasksToProcess); // When tools are configured (e.g., sendEmail), use non-generic GetResponseAsync // to allow tool execution. The generic version uses structured output which @@ -348,9 +347,9 @@ public async Task> ProcessAsync( // to produce structured JSON output. if (tools is not null && tools.Count > 0) { - if (_logger.IsEnabled(LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Information)) { - _logger.LogDebug( + _logger.LogInformation( "Post-session processing for session '{SessionId}' using tools path with {ToolCount} tool(s): [{ToolNames}].", session.SessionId, tools.Count, @@ -360,9 +359,9 @@ public async Task> ProcessAsync( return await ProcessWithToolsAsync(session, chatClient, messages, tools, tasksToProcess, cancellationToken); } - if (_logger.IsEnabled(LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Information)) { - _logger.LogDebug( + _logger.LogInformation( "Post-session processing for session '{SessionId}' using structured output path (no tools configured or resolved).", session.SessionId); } @@ -415,15 +414,18 @@ private async Task> ProcessWithToolsAsync( .Count() ?? 0; // Log tool invocation details from the response messages. - if (_logger.IsEnabled(LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Information)) { - _logger.LogDebug( + _logger.LogInformation( "Post-session tools response for session '{SessionId}': MessageCount={MessageCount}, ToolCalls={ToolCallCount}, ToolResults={ToolResultCount}.", session.SessionId, response.Messages?.Count ?? 0, toolCallCount, toolResultCount); + } + if (_logger.IsEnabled(LogLevel.Debug)) + { LogResponseMessages(session.SessionId, "tools", response.Messages); } @@ -1125,39 +1127,6 @@ private static string CreateTaskResultSummary(IEnumerable return string.Join("; ", summaries); } - private static string[] GetConfiguredToolNames( - IEnumerable profileToolNames, - IEnumerable tasks) - { - var configuredNames = new HashSet(StringComparer.OrdinalIgnoreCase); - - if (profileToolNames != null) - { - foreach (var toolName in profileToolNames) - { - if (!string.IsNullOrWhiteSpace(toolName)) - { - configuredNames.Add(toolName); - } - } - } - - if (tasks != null) - { - foreach (var toolName in tasks - .Where(task => task?.ToolNames != null) - .SelectMany(task => task.ToolNames)) - { - if (!string.IsNullOrWhiteSpace(toolName)) - { - configuredNames.Add(toolName); - } - } - } - - return configuredNames.Count > 0 ? [.. configuredNames] : []; - } - private Dictionary ApplyResults( List tasks, List results) @@ -1271,13 +1240,18 @@ private async Task GetChatClientAsync(AIProfile profile) return null; } - private async Task> ResolveToolsAsync(string sessionId, string[] toolNames) + private async Task> ResolveToolsAsync( + string sessionId, + string[] profileToolNames, + List tasks) { + var toolNames = CollectToolNames(profileToolNames, tasks); + if (toolNames is null || toolNames.Length == 0) { - if (_logger.IsEnabled(LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Information)) { - _logger.LogDebug( + _logger.LogInformation( "No tool names configured for post-session processing of session '{SessionId}'.", sessionId); } @@ -1285,37 +1259,114 @@ private async Task> ResolveToolsAsync(string sessionId, string[] t return null; } - if (_logger.IsEnabled(LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Information)) { - _logger.LogDebug( + _logger.LogInformation( "Resolving {ToolCount} tool(s) for post-session processing of session '{SessionId}': [{ToolNames}].", toolNames.Length, sessionId, string.Join(", ", toolNames)); } - var tools = new List(); + var completionContext = new AICompletionContext + { + ToolNames = toolNames, + }; + + var entries = await _toolRegistry.GetAllAsync(completionContext); - foreach (var name in toolNames) + if (entries.Count == 0) { - var tool = await _toolsService.GetByNameAsync(name); + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning( + "Tool registry returned no entries for post-session processing of session '{SessionId}'. Requested tool names: [{ToolNames}].", + sessionId, + string.Join(", ", toolNames)); + } + + return null; + } + + var tools = new List(); - if (tool is not null) + foreach (var entry in entries) + { + try { - tools.Add(tool); + var tool = await entry.CreateAsync(_serviceProvider); + + if (tool is not null) + { + tools.Add(tool); + } + else + { + _logger.LogWarning( + "Post-session tool '{ToolName}' could not be created for session '{SessionId}'.", + entry.Name, + sessionId); + } } - else + catch (Exception ex) when (ex is not OperationCanceledException) { _logger.LogWarning( - "Post-session tool '{ToolName}' could not be resolved for session '{SessionId}'. Ensure the tool is registered and its feature is enabled.", - name, + ex, + "Post-session tool '{ToolName}' failed to create for session '{SessionId}'.", + entry.Name, sessionId); } } + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug( + "Resolved {ToolCount} tool(s) for post-session processing of session '{SessionId}': [{ToolNames}].", + tools.Count, + sessionId, + string.Join(", ", tools.Select(t => t.Name))); + } + return tools.Count > 0 ? tools : null; } + private static string[] CollectToolNames( + string[] profileToolNames, + List tasks) + { + var toolNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + if (profileToolNames is not null) + { + foreach (var name in profileToolNames) + { + if (!string.IsNullOrWhiteSpace(name)) + { + toolNames.Add(name); + } + } + } + + if (tasks is not null) + { + foreach (var task in tasks) + { + if (task.ToolNames is not null) + { + foreach (var name in task.ToolNames) + { + if (!string.IsNullOrWhiteSpace(name)) + { + toolNames.Add(name); + } + } + } + } + } + + return toolNames.Count > 0 ? [.. toolNames] : []; + } + private async Task RenderTranscriptAsync( string templateId, IReadOnlyList prompts, diff --git a/src/Primitives/CrestApps.Core.AI/Templates/Prompts/post-session-analysis-prompt.md b/src/Primitives/CrestApps.Core.AI/Templates/Prompts/post-session-analysis-prompt.md index 91a7e40f..c94b13f2 100644 --- a/src/Primitives/CrestApps.Core.AI/Templates/Prompts/post-session-analysis-prompt.md +++ b/src/Primitives/CrestApps.Core.AI/Templates/Prompts/post-session-analysis-prompt.md @@ -10,7 +10,7 @@ Parameters: Analyze the following completed chat conversation and produce results for the requested tasks. Return exactly one structured result for each task listed below. Do not omit tasks, and do not return an empty tasks array. If a task does not need a tool call, still return its result value. -IMPORTANT: If you call any tools, you MUST still return the JSON output with the "tasks" array as your final response after all tool calls complete. +IMPORTANT: If a task's instructions tell you to call a tool (e.g., sendEmail), you MUST call that tool. Execute all required tool calls FIRST, then return the JSON output with the "tasks" array as your final response. Tasks to process: {% for task in tasks %} diff --git a/src/Primitives/CrestApps.Core.AI/Templates/Prompts/post-session-analysis.md b/src/Primitives/CrestApps.Core.AI/Templates/Prompts/post-session-analysis.md index ae6a8750..0d50279c 100644 --- a/src/Primitives/CrestApps.Core.AI/Templates/Prompts/post-session-analysis.md +++ b/src/Primitives/CrestApps.Core.AI/Templates/Prompts/post-session-analysis.md @@ -11,11 +11,12 @@ You are a post-session analysis assistant. Your job is to analyze a completed ch 1. Analyze the ENTIRE conversation transcript provided. 2. For PredefinedOptions tasks: select the best matching option(s) from the provided list. Use the option descriptions to guide your selection. If "allowMultiple" is true, you may select more than one option separated by commas. If false, select exactly one. 3. For Semantic tasks: follow the provided instructions and produce a freeform text result. -4. Return ONLY valid JSON only. Do NOT wrap the response in markdown code fences (```). No explanations, no comments. -5. Return exactly one result for every requested task, using the same task name. -6. Never return an empty "tasks" array. If a task does not require a tool call, still return the task result value. -7. Only return tasks that were requested. -8. If you are given tools and you call them, you MUST still produce the JSON output below AFTER all tool calls have completed. Tool execution does not replace the required JSON response. Your final message MUST always be the JSON output. +4. When tools are available and a task's instructions reference calling a tool, you MUST call that tool as part of processing the task. Do NOT skip tool calls that the task instructions require. Execute all required tool calls BEFORE producing your final response. +5. Return ONLY valid JSON only. Do NOT wrap the response in markdown code fences (```). No explanations, no comments. +6. Return exactly one result for every requested task, using the same task name. +7. Never return an empty "tasks" array. If a task does not require a tool call, still return the task result value. +8. Only return tasks that were requested. +9. After all tool calls have completed, you MUST still produce the JSON output below as your final message. Tool execution does not replace the required JSON response. [Output Format] { diff --git a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIProfiles/Create.razor b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIProfiles/Create.razor index ff02280d..9d4b1297 100644 --- a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIProfiles/Create.razor +++ b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIProfiles/Create.razor @@ -1026,58 +1026,7 @@

Select the capabilities available to this task.

-
AI Agents
- @if (_model.AvailableAgents.Count == 0) - { -

No agent profiles available.

- } - else - { - @foreach (var agent in _model.AvailableAgents) - { -
- - -
- } - } -
A2A Hosts
- @if (_model.AvailableA2AConnections.Count == 0) - { -

No A2A hosts configured.

- } - else - { - @foreach (var conn in _model.AvailableA2AConnections) - { -
- - -
- } - } -
MCP Hosts
- @if (_model.AvailableMcpConnections.Count == 0) - { -

No MCP hosts configured.

- } - else - { - @foreach (var conn in _model.AvailableMcpConnections) - { -
- - -
- } - } -
AI Tools
+
AI Tools
@if (_model.AvailableTools.Count == 0) {

No AI tools registered.

@@ -1354,30 +1303,6 @@ private string GetTaskActiveTab(int index) => _taskActiveTabs.TryGetValue(index, out var tab) ? tab : "info"; private void SetTaskActiveTab(int index, string tab) => _taskActiveTabs[index] = tab; - private void ToggleTaskAgent(PostSessionTaskItem task, string name, bool selected) - { - var list = task.SelectedAgentNames.ToList(); - if (selected && !list.Contains(name)) list.Add(name); - else if (!selected) list.Remove(name); - task.SelectedAgentNames = list.ToArray(); - } - - private void ToggleTaskA2A(PostSessionTaskItem task, string id, bool selected) - { - var list = task.SelectedA2AConnectionIds.ToList(); - if (selected && !list.Contains(id)) list.Add(id); - else if (!selected) list.Remove(id); - task.SelectedA2AConnectionIds = list.ToArray(); - } - - private void ToggleTaskMcp(PostSessionTaskItem task, string id, bool selected) - { - var list = task.SelectedMcpConnectionIds.ToList(); - if (selected && !list.Contains(id)) list.Add(id); - else if (!selected) list.Remove(id); - task.SelectedMcpConnectionIds = list.ToArray(); - } - private void ToggleTaskTool(PostSessionTaskItem task, string name, bool selected) { var list = task.SelectedToolNames.ToList(); diff --git a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIProfiles/Edit.razor b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIProfiles/Edit.razor index b4d26652..50cbfdb4 100644 --- a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIProfiles/Edit.razor +++ b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIProfiles/Edit.razor @@ -952,58 +952,7 @@ else if (_model != null)

Select the capabilities available to this task.

-
AI Agents
- @if (_model.AvailableAgents.Count == 0) - { -

No agent profiles available.

- } - else - { - @foreach (var agent in _model.AvailableAgents) - { -
- - -
- } - } -
A2A Hosts
- @if (_model.AvailableA2AConnections.Count == 0) - { -

No A2A hosts configured.

- } - else - { - @foreach (var conn in _model.AvailableA2AConnections) - { -
- - -
- } - } -
MCP Hosts
- @if (_model.AvailableMcpConnections.Count == 0) - { -

No MCP hosts configured.

- } - else - { - @foreach (var conn in _model.AvailableMcpConnections) - { -
- - -
- } - } -
AI Tools
+
AI Tools
@if (_model.AvailableTools.Count == 0) {

No AI tools registered.

@@ -1214,30 +1163,6 @@ else if (_model != null) private string GetTaskActiveTab(int index) => _taskActiveTabs.TryGetValue(index, out var tab) ? tab : "info"; private void SetTaskActiveTab(int index, string tab) => _taskActiveTabs[index] = tab; - private void ToggleTaskAgent(PostSessionTaskItem task, string name, bool selected) - { - var list = task.SelectedAgentNames.ToList(); - if (selected && !list.Contains(name)) list.Add(name); - else if (!selected) list.Remove(name); - task.SelectedAgentNames = list.ToArray(); - } - - private void ToggleTaskA2A(PostSessionTaskItem task, string id, bool selected) - { - var list = task.SelectedA2AConnectionIds.ToList(); - if (selected && !list.Contains(id)) list.Add(id); - else if (!selected) list.Remove(id); - task.SelectedA2AConnectionIds = list.ToArray(); - } - - private void ToggleTaskMcp(PostSessionTaskItem task, string id, bool selected) - { - var list = task.SelectedMcpConnectionIds.ToList(); - if (selected && !list.Contains(id)) list.Add(id); - else if (!selected) list.Remove(id); - task.SelectedMcpConnectionIds = list.ToArray(); - } - private void ToggleTaskTool(PostSessionTaskItem task, string name, bool selected) { var list = task.SelectedToolNames.ToList(); diff --git a/src/Startup/CrestApps.Core.Blazor.Web/ViewModels/AIProfileViewModel.cs b/src/Startup/CrestApps.Core.Blazor.Web/ViewModels/AIProfileViewModel.cs index 77d9404f..e49e4e17 100644 --- a/src/Startup/CrestApps.Core.Blazor.Web/ViewModels/AIProfileViewModel.cs +++ b/src/Startup/CrestApps.Core.Blazor.Web/ViewModels/AIProfileViewModel.cs @@ -248,9 +248,6 @@ public static AIProfileViewModel FromProfile(AIProfile profile) AllowMultipleValues = t.AllowMultipleValues, Options = string.Join(Environment.NewLine, t.Options.Select(o => o.Value)), SelectedToolNames = t.ToolNames ?? [], - SelectedAgentNames = t.AgentNames ?? [], - SelectedA2AConnectionIds = t.A2AConnectionIds ?? [], - SelectedMcpConnectionIds = t.McpConnectionIds ?? [], }).ToList(), EnableUserMemory = memoryMetadata.EnableUserMemory ?? false, @@ -551,9 +548,6 @@ public void ApplyTo(AIProfile profile) .Select(o => new PostSessionTaskOption { Value = o.Trim() }) .ToList(), ToolNames = t.SelectedToolNames ?? [], - AgentNames = t.SelectedAgentNames ?? [], - A2AConnectionIds = t.SelectedA2AConnectionIds ?? [], - McpConnectionIds = t.SelectedMcpConnectionIds ?? [], }).ToList(); }); @@ -630,12 +624,6 @@ public sealed class PostSessionTaskItem public string Options { get; set; } public string[] SelectedToolNames { get; set; } = []; - - public string[] SelectedAgentNames { get; set; } = []; - - public string[] SelectedA2AConnectionIds { get; set; } = []; - - public string[] SelectedMcpConnectionIds { get; set; } = []; } public sealed class PromptTemplateSelectionItem diff --git a/src/Startup/CrestApps.Core.Blazor.Web/ViewModels/AITemplateViewModel.cs b/src/Startup/CrestApps.Core.Blazor.Web/ViewModels/AITemplateViewModel.cs index 82aa436f..f844cb7c 100644 --- a/src/Startup/CrestApps.Core.Blazor.Web/ViewModels/AITemplateViewModel.cs +++ b/src/Startup/CrestApps.Core.Blazor.Web/ViewModels/AITemplateViewModel.cs @@ -318,9 +318,6 @@ public static AITemplateViewModel FromTemplate(AIProfileTemplate template) AllowMultipleValues = t.AllowMultipleValues, Options = string.Join(Environment.NewLine, t.Options.Select(o => o.Value)), SelectedToolNames = t.ToolNames ?? [], - SelectedAgentNames = t.AgentNames ?? [], - SelectedA2AConnectionIds = t.A2AConnectionIds ?? [], - SelectedMcpConnectionIds = t.McpConnectionIds ?? [], }) .ToList(); } @@ -524,9 +521,6 @@ public void ApplyTo(AIProfileTemplate template) .Select(o => new PostSessionTaskOption { Value = o.Trim() }) .ToList(), ToolNames = t.SelectedToolNames ?? [], - AgentNames = t.SelectedAgentNames ?? [], - A2AConnectionIds = t.SelectedA2AConnectionIds ?? [], - McpConnectionIds = t.SelectedMcpConnectionIds ?? [], }) .ToList(), }); diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AIProfileViewModel.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AIProfileViewModel.cs index f5f30caf..59689731 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AIProfileViewModel.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AIProfileViewModel.cs @@ -235,9 +235,6 @@ public static AIProfileViewModel FromProfile(AIProfile profile) AllowMultipleValues = t.AllowMultipleValues, Options = string.Join(Environment.NewLine, t.Options.Select(o => o.Value)), SelectedToolNames = t.ToolNames ?? [], - SelectedAgentNames = t.AgentNames ?? [], - SelectedA2AConnectionIds = t.A2AConnectionIds ?? [], - SelectedMcpConnectionIds = t.McpConnectionIds ?? [], }).ToList(), EnableUserMemory = memoryMetadata.EnableUserMemory ?? false, }; @@ -539,9 +536,6 @@ public void ApplyTo(AIProfile profile) .Select(o => new PostSessionTaskOption { Value = o.Trim() }) .ToList(), ToolNames = t.SelectedToolNames ?? [], - AgentNames = t.SelectedAgentNames ?? [], - A2AConnectionIds = t.SelectedA2AConnectionIds ?? [], - McpConnectionIds = t.SelectedMcpConnectionIds ?? [], }).ToList(); }); @@ -619,12 +613,6 @@ public sealed class PostSessionTaskItem public string Options { get; set; } public string[] SelectedToolNames { get; set; } = []; - - public string[] SelectedAgentNames { get; set; } = []; - - public string[] SelectedA2AConnectionIds { get; set; } = []; - - public string[] SelectedMcpConnectionIds { get; set; } = []; } public sealed class PromptTemplateSelectionItem diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AITemplateViewModel.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AITemplateViewModel.cs index c4995df8..bd320f18 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AITemplateViewModel.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AITemplateViewModel.cs @@ -302,9 +302,6 @@ public static AITemplateViewModel FromTemplate(AIProfileTemplate template) AllowMultipleValues = t.AllowMultipleValues, Options = string.Join(Environment.NewLine, t.Options.Select(o => o.Value)), SelectedToolNames = t.ToolNames ?? [], - SelectedAgentNames = t.AgentNames ?? [], - SelectedA2AConnectionIds = t.A2AConnectionIds ?? [], - SelectedMcpConnectionIds = t.McpConnectionIds ?? [], }) .ToList(); } @@ -508,9 +505,6 @@ public void ApplyTo(AIProfileTemplate template) .Select(o => new PostSessionTaskOption { Value = o.Trim() }) .ToList(), ToolNames = t.SelectedToolNames ?? [], - AgentNames = t.SelectedAgentNames ?? [], - A2AConnectionIds = t.SelectedA2AConnectionIds ?? [], - McpConnectionIds = t.SelectedMcpConnectionIds ?? [], }) .ToList(), }); diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Create.cshtml b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Create.cshtml index d267d171..ce9b9c56 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Create.cshtml +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Create.cshtml @@ -1170,24 +1170,12 @@ let taskIndex = 0; const taskCapabilities = { - agents: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableAgents.Select(a => new { a.Name, a.DisplayText }))), - a2a: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableA2AConnections.Select(c => new { c.ItemId, c.DisplayText }))), - mcp: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableMcpConnections.Select(c => new { c.ItemId, c.DisplayText }))), tools: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableTools.GroupBy(t => t.Category).OrderBy(g => g.Key).Select(g => new { Category = g.Key, Items = g.Select(t => new { t.Name, t.Title }) }))) }; function buildTaskCapabilitiesHtml(idx) { let html = '

Select the capabilities available to this task.

'; - html += '
AI Agents
'; - if (taskCapabilities.agents.length === 0) { html += '

No agent profiles available.

'; } - else { taskCapabilities.agents.forEach(a => { html += `
`; }); } - html += '
A2A Hosts
'; - if (taskCapabilities.a2a.length === 0) { html += '

No A2A hosts configured.

'; } - else { taskCapabilities.a2a.forEach(c => { html += `
`; }); } - html += '
MCP Hosts
'; - if (taskCapabilities.mcp.length === 0) { html += '

No MCP hosts configured.

'; } - else { taskCapabilities.mcp.forEach(c => { html += `
`; }); } - html += '
AI Tools
'; + html += '
AI Tools
'; if (taskCapabilities.tools.length === 0) { html += '

No AI tools registered.

'; } else { taskCapabilities.tools.forEach(g => { html += `
${g.Category}
`; g.Items.forEach(t => { html += `
`; }); }); } return html; diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Edit.cshtml b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Edit.cshtml index c4252bc8..2d329bfa 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Edit.cshtml +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Edit.cshtml @@ -860,55 +860,7 @@

Select the capabilities available to this task.

-
AI Agents
- @if (Model.AvailableAgents.Count == 0) - { -

No agent profiles available.

- } - else - { - @foreach (var agent in Model.AvailableAgents) - { -
- - -
- } - } -
A2A Hosts
- @if (Model.AvailableA2AConnections.Count == 0) - { -

No A2A hosts configured.

- } - else - { - @foreach (var conn in Model.AvailableA2AConnections) - { -
- - -
- } - } -
MCP Hosts
- @if (Model.AvailableMcpConnections.Count == 0) - { -

No MCP hosts configured.

- } - else - { - @foreach (var conn in Model.AvailableMcpConnections) - { -
- - -
- } - } -
AI Tools
+
AI Tools
@if (Model.AvailableTools.Count == 0) {

No AI tools registered.

@@ -1416,24 +1368,12 @@ var taskIndex = @Model.PostSessionTasks.Count; var taskCapabilities = { - agents: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableAgents.Select(a => new { a.Name, a.DisplayText }))), - a2a: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableA2AConnections.Select(c => new { c.ItemId, c.DisplayText }))), - mcp: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableMcpConnections.Select(c => new { c.ItemId, c.DisplayText }))), tools: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableTools.GroupBy(t => t.Category).OrderBy(g => g.Key).Select(g => new { Category = g.Key, Items = g.Select(t => new { t.Name, t.Title }) }))) }; function buildTaskCapabilitiesHtml(idx) { var html = '

Select the capabilities available to this task.

'; - html += '
AI Agents
'; - if (taskCapabilities.agents.length === 0) { html += '

No agent profiles available.

'; } - else { taskCapabilities.agents.forEach(function (a) { html += '
'; }); } - html += '
A2A Hosts
'; - if (taskCapabilities.a2a.length === 0) { html += '

No A2A hosts configured.

'; } - else { taskCapabilities.a2a.forEach(function (c) { html += '
'; }); } - html += '
MCP Hosts
'; - if (taskCapabilities.mcp.length === 0) { html += '

No MCP hosts configured.

'; } - else { taskCapabilities.mcp.forEach(function (c) { html += '
'; }); } - html += '
AI Tools
'; + html += '
AI Tools
'; if (taskCapabilities.tools.length === 0) { html += '

No AI tools registered.

'; } else { taskCapabilities.tools.forEach(function (g) { html += '
' + g.Category + '
'; g.Items.forEach(function (t) { html += '
'; }); }); } return html; diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Create.cshtml b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Create.cshtml index dc46e74f..8e91c41b 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Create.cshtml +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Create.cshtml @@ -1064,24 +1064,12 @@ var taskIndex = 0; var taskCapabilities = { - agents: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableAgents.Select(a => new { a.Name, a.DisplayText }))), - a2a: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableA2AConnections.Select(c => new { c.ItemId, c.DisplayText }))), - mcp: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableMcpConnections.Select(c => new { c.ItemId, c.DisplayText }))), tools: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableTools.GroupBy(t => t.Category).OrderBy(g => g.Key).Select(g => new { Category = g.Key, Items = g.Select(t => new { t.Name, t.Title }) }))) }; function buildTaskCapabilitiesHtml(idx) { var html = '

Select the capabilities available to this task.

'; - html += '
AI Agents
'; - if (taskCapabilities.agents.length === 0) { html += '

No agent profiles available.

'; } - else { taskCapabilities.agents.forEach(function (a) { html += '
'; }); } - html += '
A2A Hosts
'; - if (taskCapabilities.a2a.length === 0) { html += '

No A2A hosts configured.

'; } - else { taskCapabilities.a2a.forEach(function (c) { html += '
'; }); } - html += '
MCP Hosts
'; - if (taskCapabilities.mcp.length === 0) { html += '

No MCP hosts configured.

'; } - else { taskCapabilities.mcp.forEach(function (c) { html += '
'; }); } - html += '
AI Tools
'; + html += '
AI Tools
'; if (taskCapabilities.tools.length === 0) { html += '

No AI tools registered.

'; } else { taskCapabilities.tools.forEach(function (g) { html += '
' + g.Category + '
'; g.Items.forEach(function (t) { html += '
'; }); }); } return html; diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Edit.cshtml b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Edit.cshtml index 919d7d9f..f4ce4e8b 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Edit.cshtml +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Edit.cshtml @@ -749,55 +749,7 @@

Select the capabilities available to this task.

-
AI Agents
- @if (Model.AvailableAgents.Count == 0) - { -

No agent profiles available.

- } - else - { - @foreach (var agent in Model.AvailableAgents) - { -
- - -
- } - } -
A2A Hosts
- @if (Model.AvailableA2AConnections.Count == 0) - { -

No A2A hosts configured.

- } - else - { - @foreach (var conn in Model.AvailableA2AConnections) - { -
- - -
- } - } -
MCP Hosts
- @if (Model.AvailableMcpConnections.Count == 0) - { -

No MCP hosts configured.

- } - else - { - @foreach (var conn in Model.AvailableMcpConnections) - { -
- - -
- } - } -
AI Tools
+
AI Tools
@if (Model.AvailableTools.Count == 0) {

No AI tools registered.

@@ -1340,24 +1292,12 @@ var taskIndex = @Model.PostSessionTasks.Count; var taskCapabilities = { - agents: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableAgents.Select(a => new { a.Name, a.DisplayText }))), - a2a: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableA2AConnections.Select(c => new { c.ItemId, c.DisplayText }))), - mcp: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableMcpConnections.Select(c => new { c.ItemId, c.DisplayText }))), tools: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.AvailableTools.GroupBy(t => t.Category).OrderBy(g => g.Key).Select(g => new { Category = g.Key, Items = g.Select(t => new { t.Name, t.Title }) }))) }; function buildTaskCapabilitiesHtml(idx) { var html = '

Select the capabilities available to this task.

'; - html += '
AI Agents
'; - if (taskCapabilities.agents.length === 0) { html += '

No agent profiles available.

'; } - else { taskCapabilities.agents.forEach(function (a) { html += '
'; }); } - html += '
A2A Hosts
'; - if (taskCapabilities.a2a.length === 0) { html += '

No A2A hosts configured.

'; } - else { taskCapabilities.a2a.forEach(function (c) { html += '
'; }); } - html += '
MCP Hosts
'; - if (taskCapabilities.mcp.length === 0) { html += '

No MCP hosts configured.

'; } - else { taskCapabilities.mcp.forEach(function (c) { html += '
'; }); } - html += '
AI Tools
'; + html += '
AI Tools
'; if (taskCapabilities.tools.length === 0) { html += '

No AI tools registered.

'; } else { taskCapabilities.tools.forEach(function (g) { html += '
' + g.Category + '
'; g.Items.forEach(function (t) { html += '
'; }); }); } return html; diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Program.cs b/src/Startup/CrestApps.Core.Mvc.Web/Program.cs index f5a6e7b4..c6c2ef87 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Program.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Program.cs @@ -16,6 +16,7 @@ using CrestApps.Core.AI.Mcp.Ftp; using CrestApps.Core.AI.Mcp.Models; using CrestApps.Core.AI.Mcp.Sftp; +using CrestApps.Core.AI.Models; using CrestApps.Core.AI.Ollama; using CrestApps.Core.AI.OpenAI; using CrestApps.Core.AI.OpenAI.Azure; diff --git a/tests/CrestApps.Core.Tests/ChatNotifications/EndSessionNotificationActionHandlerTests.cs b/tests/CrestApps.Core.Tests/ChatNotifications/EndSessionNotificationActionHandlerTests.cs index 60f02130..18ceb262 100644 --- a/tests/CrestApps.Core.Tests/ChatNotifications/EndSessionNotificationActionHandlerTests.cs +++ b/tests/CrestApps.Core.Tests/ChatNotifications/EndSessionNotificationActionHandlerTests.cs @@ -1,10 +1,10 @@ using CrestApps.Core.AI.Chat; using CrestApps.Core.AI.Chat.Services; -using CrestApps.Core.AI; using CrestApps.Core.AI.Clients; using CrestApps.Core.AI.Deployments; using CrestApps.Core.AI.Models; using CrestApps.Core.AI.Profiles; +using CrestApps.Core.AI.Tooling; using CrestApps.Core.Templates.Parsing; using CrestApps.Core.Templates.Services; using Microsoft.Extensions.DependencyInjection; @@ -240,7 +240,7 @@ private static AIChatSessionPostCloseProcessor CreatePostCloseProcessor(TimeProv var postSessionProcessingService = new PostSessionProcessingService( Mock.Of(), - Mock.Of(), + Mock.Of(), templateService.Object, [markdownParser.Object], new DefaultAIOptions(), diff --git a/tests/CrestApps.Core.Tests/Core/Services/PostSession/AIChatSessionPostCloseProcessorTests.cs b/tests/CrestApps.Core.Tests/Core/Services/PostSession/AIChatSessionPostCloseProcessorTests.cs index a54e914b..e09fe2d0 100644 --- a/tests/CrestApps.Core.Tests/Core/Services/PostSession/AIChatSessionPostCloseProcessorTests.cs +++ b/tests/CrestApps.Core.Tests/Core/Services/PostSession/AIChatSessionPostCloseProcessorTests.cs @@ -1,8 +1,8 @@ -using CrestApps.Core.AI; using CrestApps.Core.AI.Chat.Services; using CrestApps.Core.AI.Clients; using CrestApps.Core.AI.Deployments; using CrestApps.Core.AI.Models; +using CrestApps.Core.AI.Tooling; using CrestApps.Core.Templates.Parsing; using CrestApps.Core.Templates.Services; using Microsoft.Extensions.AI; @@ -319,7 +319,7 @@ private static PostSessionProcessingService CreatePostSessionService( return new PostSessionProcessingService( mockClientFactory.Object, - Mock.Of(), + Mock.Of(), mockTemplateService.Object, [new DefaultMarkdownTemplateParser()], new DefaultAIOptions diff --git a/tests/CrestApps.Core.Tests/Core/Services/PostSession/PostSessionProcessingServiceTests.cs b/tests/CrestApps.Core.Tests/Core/Services/PostSession/PostSessionProcessingServiceTests.cs index df02aa98..2620c80c 100644 --- a/tests/CrestApps.Core.Tests/Core/Services/PostSession/PostSessionProcessingServiceTests.cs +++ b/tests/CrestApps.Core.Tests/Core/Services/PostSession/PostSessionProcessingServiceTests.cs @@ -3,6 +3,7 @@ using CrestApps.Core.AI.Clients; using CrestApps.Core.AI.Deployments; using CrestApps.Core.AI.Models; +using CrestApps.Core.AI.Tooling; using CrestApps.Core.Templates.Parsing; using CrestApps.Core.Templates.Services; using Microsoft.Extensions.AI; @@ -154,8 +155,8 @@ public async Task ProcessAsync_WithTaskScopedToolNames_ShouldResolveToolsAndUseT var session = CreateSession(); var prompts = CreatePrompts(); var mockTool = new TestAIFunction("sendEmail"); - var mockToolsService = new Mock(); - mockToolsService.Setup(t => t.GetByNameAsync("sendEmail")).ReturnsAsync(mockTool); + var mockToolRegistry = new Mock(); + mockToolRegistry.Setup(t => t.GetAllAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List { CreateToolEntry("sendEmail", mockTool) }); var mockChatClient = new Mock(); // The tools path calls non-generic GetResponseAsync. // Simulate response with a JSON result. @@ -168,13 +169,13 @@ public async Task ProcessAsync_WithTaskScopedToolNames_ShouldResolveToolsAndUseT mockTemplateService.Setup(t => t .RenderAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync("Rendered prompt"); - var service = CreateService(chatClient: mockChatClient.Object, toolsService: mockToolsService.Object, templateService: mockTemplateService.Object); + var service = CreateService(chatClient: mockChatClient.Object, toolRegistry: mockToolRegistry.Object, templateService: mockTemplateService.Object); // Act var result = await service.ProcessAsync(profile, session, prompts, TestContext.Current.CancellationToken); // Assert: tools service was asked to resolve the tool. - mockToolsService.Verify(t => t.GetByNameAsync("sendEmail"), Times.Once); + mockToolRegistry.Verify(t => t.GetAllAsync(It.IsAny(), It.IsAny()), Times.Once); // Assert: the chat client was invoked with tools in the options. mockChatClient.Verify(c => c.GetResponseAsync(It.IsAny>(), It.Is(opts => opts.Tools != null && opts.Tools.Count > 0), It.IsAny()), Times.Once); @@ -198,8 +199,8 @@ public async Task ProcessAsync_WhenToolResponseContainsOnlyInvalidTaskEntriesWit var session = CreateSession(); var prompts = CreatePrompts(); var mockTool = new TestAIFunction("sendEmail"); - var mockToolsService = new Mock(); - mockToolsService.Setup(t => t.GetByNameAsync("sendEmail")).ReturnsAsync(mockTool); + var mockToolRegistry = new Mock(); + mockToolRegistry.Setup(t => t.GetAllAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List { CreateToolEntry("sendEmail", mockTool) }); var mockChatClient = new Mock(); mockChatClient.SetupSequence(c => c .GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny())) @@ -210,7 +211,7 @@ public async Task ProcessAsync_WhenToolResponseContainsOnlyInvalidTaskEntriesWit mockTemplateService.Setup(t => t .RenderAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync("Rendered prompt"); - var service = CreateService(chatClient: mockChatClient.Object, toolsService: mockToolsService.Object, templateService: mockTemplateService.Object); + var service = CreateService(chatClient: mockChatClient.Object, toolRegistry: mockToolRegistry.Object, templateService: mockTemplateService.Object); var result = await service.ProcessAsync(profile, session, prompts, TestContext.Current.CancellationToken); @@ -239,8 +240,8 @@ public async Task ProcessAsync_WhenToolResponseContainsEmptyTasksArray_ShouldRet var session = CreateSession(); var prompts = CreatePrompts(); var mockTool = new TestAIFunction("sendEmail"); - var mockToolsService = new Mock(); - mockToolsService.Setup(t => t.GetByNameAsync("sendEmail")).ReturnsAsync(mockTool); + var mockToolRegistry = new Mock(); + mockToolRegistry.Setup(t => t.GetAllAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List { CreateToolEntry("sendEmail", mockTool) }); var mockChatClient = new Mock(); mockChatClient.Setup(c => c .GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny())) @@ -249,7 +250,7 @@ public async Task ProcessAsync_WhenToolResponseContainsEmptyTasksArray_ShouldRet mockTemplateService.Setup(t => t .RenderAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync("Rendered prompt"); - var service = CreateService(chatClient: mockChatClient.Object, toolsService: mockToolsService.Object, templateService: mockTemplateService.Object); + var service = CreateService(chatClient: mockChatClient.Object, toolRegistry: mockToolRegistry.Object, templateService: mockTemplateService.Object); var result = await service.ProcessAsync(profile, session, prompts, TestContext.Current.CancellationToken); @@ -277,8 +278,8 @@ public async Task ProcessAsync_WhenToolNotFound_ShouldLogWarningAndFallToStructu }); var session = CreateSession(); var prompts = CreatePrompts(); - var mockToolsService = new Mock(); - mockToolsService.Setup(t => t.GetByNameAsync("nonExistentTool")).ReturnsAsync((AITool)null); + var mockToolRegistry = new Mock(); + mockToolRegistry.Setup(t => t.GetAllAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List()); var mockChatClient = new Mock(); mockChatClient.Setup(c => c .GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny())) @@ -287,13 +288,13 @@ public async Task ProcessAsync_WhenToolNotFound_ShouldLogWarningAndFallToStructu mockTemplateService.Setup(t => t .RenderAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync("Rendered prompt"); - var service = CreateService(chatClient: mockChatClient.Object, toolsService: mockToolsService.Object, templateService: mockTemplateService.Object); + var service = CreateService(chatClient: mockChatClient.Object, toolRegistry: mockToolRegistry.Object, templateService: mockTemplateService.Object); // Act var result = await service.ProcessAsync(profile, session, prompts, TestContext.Current.CancellationToken); // Assert: tool resolution was attempted. - mockToolsService.Verify(t => t.GetByNameAsync("nonExistentTool"), Times.Once); + mockToolRegistry.Verify(t => t.GetAllAsync(It.IsAny(), It.IsAny()), Times.Once); // Assert: when no tools resolve, the structured output path is used (no tools in options). mockChatClient.Verify(c => c.GetResponseAsync(It.IsAny>(), It.Is(opts => opts.Tools == null || opts.Tools.Count == 0), It.IsAny()), Times.Once); @@ -557,7 +558,7 @@ private static List CreateAssistantOnlyPrompts() ]; } - private static PostSessionProcessingService CreateService(IChatClient chatClient = null, IAIToolsService toolsService = null, ITemplateService templateService = null) + private static PostSessionProcessingService CreateService(IChatClient chatClient = null, IToolRegistry toolRegistry = null, ITemplateService templateService = null) { var mockClientFactory = new Mock(); if (chatClient is not null) @@ -583,7 +584,7 @@ private static PostSessionProcessingService CreateService(IChatClient chatClient .ReturnsAsync(deployment); } - var mockToolsService = toolsService is not null ? null : new Mock(); + var mockToolRegistry = toolRegistry is not null ? null : new Mock(); var mockTemplateService = templateService is not null ? null : new Mock(); // Set up default template renders. if (mockTemplateService is not null) @@ -600,7 +601,7 @@ private static PostSessionProcessingService CreateService(IChatClient chatClient return new PostSessionProcessingService( mockClientFactory.Object, - toolsService ?? mockToolsService.Object, + toolRegistry ?? mockToolRegistry.Object, templateService ?? mockTemplateService.Object, [new DefaultMarkdownTemplateParser()], defaultOptions, @@ -637,6 +638,17 @@ protected override ValueTask InvokeCoreAsync(AIFunctionArguments argumen } } + private static ToolRegistryEntry CreateToolEntry(string name, AITool tool) + { + return new ToolRegistryEntry + { + Id = name, + Name = name, + Source = ToolRegistryEntrySource.Local, + CreateAsync = _ => new ValueTask(tool), + }; + } + [Fact] public async Task ProcessAsync_WithTools_WhenResponseIsJsonInCodeFence_ShouldParseSuccessfully() { @@ -656,8 +668,8 @@ public async Task ProcessAsync_WithTools_WhenResponseIsJsonInCodeFence_ShouldPar var session = CreateSession(); var prompts = CreatePrompts(); var mockTool = new TestAIFunction("sendEmail"); - var mockToolsService = new Mock(); - mockToolsService.Setup(t => t.GetByNameAsync("sendEmail")).ReturnsAsync(mockTool); + var mockToolRegistry = new Mock(); + mockToolRegistry.Setup(t => t.GetAllAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List { CreateToolEntry("sendEmail", mockTool) }); var responseText = "```json\n{\"tasks\":[{\"name\":\"summary\",\"value\":\"Customer asked about pricing.\"}]}\n```"; var mockChatClient = new Mock(); mockChatClient.Setup(c => c @@ -667,7 +679,7 @@ public async Task ProcessAsync_WithTools_WhenResponseIsJsonInCodeFence_ShouldPar mockTemplateService.Setup(t => t .RenderAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync("Rendered prompt"); - var service = CreateService(chatClient: mockChatClient.Object, toolsService: mockToolsService.Object, templateService: mockTemplateService.Object); + var service = CreateService(chatClient: mockChatClient.Object, toolRegistry: mockToolRegistry.Object, templateService: mockTemplateService.Object); // Act var result = await service.ProcessAsync(profile, session, prompts, TestContext.Current.CancellationToken); @@ -698,8 +710,8 @@ public async Task ProcessAsync_WithTools_WhenResponseIsJsonWithSurroundingText_S var session = CreateSession(); var prompts = CreatePrompts(); var mockTool = new TestAIFunction("sendEmail"); - var mockToolsService = new Mock(); - mockToolsService.Setup(t => t.GetByNameAsync("sendEmail")).ReturnsAsync(mockTool); + var mockToolRegistry = new Mock(); + mockToolRegistry.Setup(t => t.GetAllAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List { CreateToolEntry("sendEmail", mockTool) }); var responseText = "Here are the results:\n{\"tasks\":[{\"name\":\"summary\",\"value\":\"Customer asked about pricing.\"}]}\nDone."; var mockChatClient = new Mock(); mockChatClient.Setup(c => c @@ -709,7 +721,7 @@ public async Task ProcessAsync_WithTools_WhenResponseIsJsonWithSurroundingText_S mockTemplateService.Setup(t => t .RenderAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync("Rendered prompt"); - var service = CreateService(chatClient: mockChatClient.Object, toolsService: mockToolsService.Object, templateService: mockTemplateService.Object); + var service = CreateService(chatClient: mockChatClient.Object, toolRegistry: mockToolRegistry.Object, templateService: mockTemplateService.Object); // Act var result = await service.ProcessAsync(profile, session, prompts, TestContext.Current.CancellationToken); @@ -740,8 +752,8 @@ public async Task ProcessAsync_WithTools_WhenAssistantResponseUsesContentsText_S var session = CreateSession(); var prompts = CreatePrompts(); var mockTool = new TestAIFunction("sendEmail"); - var mockToolsService = new Mock(); - mockToolsService.Setup(t => t.GetByNameAsync("sendEmail")).ReturnsAsync(mockTool); + var mockToolRegistry = new Mock(); + mockToolRegistry.Setup(t => t.GetAllAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List { CreateToolEntry("sendEmail", mockTool) }); var responseMessage = new ChatMessage { Role = ChatRole.Assistant, @@ -755,7 +767,7 @@ public async Task ProcessAsync_WithTools_WhenAssistantResponseUsesContentsText_S mockTemplateService.Setup(t => t .RenderAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync("Rendered prompt"); - var service = CreateService(chatClient: mockChatClient.Object, toolsService: mockToolsService.Object, templateService: mockTemplateService.Object); + var service = CreateService(chatClient: mockChatClient.Object, toolRegistry: mockToolRegistry.Object, templateService: mockTemplateService.Object); // Act var result = await service.ProcessAsync(profile, session, prompts, TestContext.Current.CancellationToken); @@ -786,8 +798,8 @@ public async Task ProcessAsync_WithTools_WhenResponseIsTruncatedJson_ShouldRecov var session = CreateSession(); var prompts = CreatePrompts(); var mockTool = new TestAIFunction("sendEmail"); - var mockToolsService = new Mock(); - mockToolsService.Setup(t => t.GetByNameAsync("sendEmail")).ReturnsAsync(mockTool); + var mockToolRegistry = new Mock(); + mockToolRegistry.Setup(t => t.GetAllAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List { CreateToolEntry("sendEmail", mockTool) }); var mockChatClient = new Mock(); mockChatClient.SetupSequence(c => c .GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny())) @@ -797,7 +809,7 @@ public async Task ProcessAsync_WithTools_WhenResponseIsTruncatedJson_ShouldRecov mockTemplateService.Setup(t => t .RenderAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync("Rendered prompt"); - var service = CreateService(chatClient: mockChatClient.Object, toolsService: mockToolsService.Object, templateService: mockTemplateService.Object); + var service = CreateService(chatClient: mockChatClient.Object, toolRegistry: mockToolRegistry.Object, templateService: mockTemplateService.Object); // Act var result = await service.ProcessAsync(profile, session, prompts, TestContext.Current.CancellationToken); @@ -829,8 +841,8 @@ public async Task ProcessAsync_WithTools_WhenStructuredRetryReturnsMarkdownWrapp var session = CreateSession(); var prompts = CreatePrompts(); var mockTool = new TestAIFunction("sendEmail"); - var mockToolsService = new Mock(); - mockToolsService.Setup(t => t.GetByNameAsync("sendEmail")).ReturnsAsync(mockTool); + var mockToolRegistry = new Mock(); + mockToolRegistry.Setup(t => t.GetAllAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List { CreateToolEntry("sendEmail", mockTool) }); var mockChatClient = new Mock(); mockChatClient.SetupSequence(c => c .GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny())) @@ -854,7 +866,7 @@ public async Task ProcessAsync_WithTools_WhenStructuredRetryReturnsMarkdownWrapp mockTemplateService.Setup(t => t .RenderAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync("Rendered prompt"); - var service = CreateService(chatClient: mockChatClient.Object, toolsService: mockToolsService.Object, templateService: mockTemplateService.Object); + var service = CreateService(chatClient: mockChatClient.Object, toolRegistry: mockToolRegistry.Object, templateService: mockTemplateService.Object); // Act var result = await service.ProcessAsync(profile, session, prompts, TestContext.Current.CancellationToken); @@ -886,8 +898,8 @@ public async Task ProcessAsync_WithTools_WhenSingleSemanticTaskAndNonJsonRespons var session = CreateSession(); var prompts = CreatePrompts(); var mockTool = new TestAIFunction("sendEmail"); - var mockToolsService = new Mock(); - mockToolsService.Setup(t => t.GetByNameAsync("sendEmail")).ReturnsAsync(mockTool); + var mockToolRegistry = new Mock(); + mockToolRegistry.Setup(t => t.GetAllAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List { CreateToolEntry("sendEmail", mockTool) }); var responseText = "The customer asked about pricing options."; var mockChatClient = new Mock(); mockChatClient.Setup(c => c @@ -897,7 +909,7 @@ public async Task ProcessAsync_WithTools_WhenSingleSemanticTaskAndNonJsonRespons mockTemplateService.Setup(t => t .RenderAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync("Rendered prompt"); - var service = CreateService(chatClient: mockChatClient.Object, toolsService: mockToolsService.Object, templateService: mockTemplateService.Object); + var service = CreateService(chatClient: mockChatClient.Object, toolRegistry: mockToolRegistry.Object, templateService: mockTemplateService.Object); // Act var result = await service.ProcessAsync(profile, session, prompts, TestContext.Current.CancellationToken); @@ -939,8 +951,8 @@ public async Task ProcessAsync_WithTools_WhenMultipleTasksAndNonJsonResponse_Sho var session = CreateSession(); var prompts = CreatePrompts(); var mockTool = new TestAIFunction("sendEmail"); - var mockToolsService = new Mock(); - mockToolsService.Setup(t => t.GetByNameAsync("sendEmail")).ReturnsAsync(mockTool); + var mockToolRegistry = new Mock(); + mockToolRegistry.Setup(t => t.GetAllAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List { CreateToolEntry("sendEmail", mockTool) }); var mockChatClient = new Mock(); mockChatClient.Setup(c => c .GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny())) @@ -949,7 +961,7 @@ public async Task ProcessAsync_WithTools_WhenMultipleTasksAndNonJsonResponse_Sho mockTemplateService.Setup(t => t .RenderAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync("Rendered prompt"); - var service = CreateService(chatClient: mockChatClient.Object, toolsService: mockToolsService.Object, templateService: mockTemplateService.Object); + var service = CreateService(chatClient: mockChatClient.Object, toolRegistry: mockToolRegistry.Object, templateService: mockTemplateService.Object); // Act var result = await service.ProcessAsync(profile, session, prompts, TestContext.Current.CancellationToken); @@ -982,8 +994,8 @@ public async Task ProcessAsync_WithTools_WhenResponseIsEmpty_ShouldReturnFailedR var session = CreateSession(); var prompts = CreatePrompts(); var mockTool = new TestAIFunction("sendEmail"); - var mockToolsService = new Mock(); - mockToolsService.Setup(t => t.GetByNameAsync("sendEmail")).ReturnsAsync(mockTool); + var mockToolRegistry = new Mock(); + mockToolRegistry.Setup(t => t.GetAllAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List { CreateToolEntry("sendEmail", mockTool) }); var mockChatClient = new Mock(); mockChatClient.Setup(c => c .GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny())) @@ -992,7 +1004,7 @@ public async Task ProcessAsync_WithTools_WhenResponseIsEmpty_ShouldReturnFailedR mockTemplateService.Setup(t => t .RenderAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync("Rendered prompt"); - var service = CreateService(chatClient: mockChatClient.Object, toolsService: mockToolsService.Object, templateService: mockTemplateService.Object); + var service = CreateService(chatClient: mockChatClient.Object, toolRegistry: mockToolRegistry.Object, templateService: mockTemplateService.Object); // Act var result = await service.ProcessAsync(profile, session, prompts, TestContext.Current.CancellationToken); diff --git a/tests/CrestApps.Core.Tests/Framework/Mvc/AIChatHubCoreTests.cs b/tests/CrestApps.Core.Tests/Framework/Mvc/AIChatHubCoreTests.cs index 6520112f..a521dcbf 100644 --- a/tests/CrestApps.Core.Tests/Framework/Mvc/AIChatHubCoreTests.cs +++ b/tests/CrestApps.Core.Tests/Framework/Mvc/AIChatHubCoreTests.cs @@ -90,7 +90,7 @@ public static bool IsEndedStatusForTest(ChatSessionStatus status) Assert.NotNull(method); - return (bool)method.Invoke(null, new object[] { status }); + return (bool)method.Invoke(null, [status]); } }