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
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,4 @@ public sealed class PostSessionTask
/// Gets or sets the AI tool names available to this task during post-session processing.
/// </summary>
public string[] ToolNames { get; set; } = [];

/// <summary>
/// Gets or sets the AI agent profile names available to this task during post-session processing.
/// </summary>
public string[] AgentNames { get; set; } = [];

/// <summary>
/// Gets or sets the A2A connection identifiers available to this task during post-session processing.
/// </summary>
public string[] A2AConnectionIds { get; set; } = [];

/// <summary>
/// Gets or sets the MCP connection identifiers available to this task during post-session processing.
/// </summary>
public string[] McpConnectionIds { get; set; } = [];
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public sealed class AIChatSessionCloseRunner
private static readonly TimeSpan _interval = TimeSpan.FromMinutes(5);

private readonly AIChatSessionCloseCycleService _cycleService;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<AIChatSessionCloseRunner> _logger;
private readonly Lock _syncLock = new();

Expand All @@ -22,12 +23,15 @@ public sealed class AIChatSessionCloseRunner
/// Initializes a new instance of the <see cref="AIChatSessionCloseRunner"/> class.
/// </summary>
/// <param name="cycleService">The shared cycle service.</param>
/// <param name="serviceProvider">The service provider.</param>
/// <param name="logger">The logger.</param>
public AIChatSessionCloseRunner(
AIChatSessionCloseCycleService cycleService,
IServiceProvider serviceProvider,
ILogger<AIChatSessionCloseRunner> logger)
{
_cycleService = cycleService;
_serviceProvider = serviceProvider;
_logger = logger;
}

Expand Down Expand Up @@ -104,20 +108,20 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
/// Runs the shared AI chat session close cycle immediately.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -35,7 +36,7 @@ public sealed class PostSessionProcessingService
/// Initializes a new instance of the <see cref="PostSessionProcessingService"/> class.
/// </summary>
/// <param name="clientFactory">The client factory.</param>
/// <param name="toolsService">The tools service.</param>
/// <param name="toolRegistry">The tool registry.</param>
/// <param name="aiTemplateService">The ai template service.</param>
/// <param name="templateParsers">The registered template parsers.</param>
/// <param name="defaultOptions">The default options.</param>
Expand All @@ -45,7 +46,7 @@ public sealed class PostSessionProcessingService
/// <param name="deploymentManager">The deployment manager.</param>
public PostSessionProcessingService(
IAIClientFactory clientFactory,
IAIToolsService toolsService,
IToolRegistry toolRegistry,
ITemplateService aiTemplateService,
IEnumerable<ITemplateParser> templateParsers,
DefaultAIOptions defaultOptions,
Expand All @@ -56,7 +57,7 @@ public PostSessionProcessingService(
{
_clientFactory = clientFactory;
_deploymentManager = deploymentManager;
_toolsService = toolsService;
_toolRegistry = toolRegistry;
_aiTemplateService = aiTemplateService;
_markdownTemplateParser = ResolveMarkdownTemplateParser(templateParsers);
_serviceProvider = serviceProvider;
Expand Down Expand Up @@ -338,19 +339,17 @@ public async Task<Dictionary<string, PostSessionResult>> 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
// conflicts with tool calls - the model may fail to call tools when forced
// 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,
Expand All @@ -360,9 +359,9 @@ public async Task<Dictionary<string, PostSessionResult>> 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);
}
Expand Down Expand Up @@ -415,15 +414,18 @@ private async Task<Dictionary<string, PostSessionResult>> 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);
}

Expand Down Expand Up @@ -1125,39 +1127,6 @@ private static string CreateTaskResultSummary(IEnumerable<PostSessionTaskResult>
return string.Join("; ", summaries);
}

private static string[] GetConfiguredToolNames(
IEnumerable<string> profileToolNames,
IEnumerable<PostSessionTask> tasks)
{
var configuredNames = new HashSet<string>(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<string, PostSessionResult> ApplyResults(
List<PostSessionTask> tasks,
List<PostSessionTaskResult> results)
Expand Down Expand Up @@ -1271,51 +1240,133 @@ private async Task<IChatClient> GetChatClientAsync(AIProfile profile)
return null;
}

private async Task<IList<AITool>> ResolveToolsAsync(string sessionId, string[] toolNames)
private async Task<IList<AITool>> ResolveToolsAsync(
string sessionId,
string[] profileToolNames,
List<PostSessionTask> 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);
}

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<AITool>();
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<AITool>();

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<PostSessionTask> tasks)
{
var toolNames = new HashSet<string>(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<string> RenderTranscriptAsync(
string templateId,
IReadOnlyList<AIChatSessionPrompt> prompts,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
{
Expand Down
Loading
Loading