Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ Keep the docs focused on `CrestApps.Core`. If you need to mention the Orchard Co
- Prefer SOLID and DRY refactors that consolidate duplicated provider, transport, or store logic into shared abstractions before adding new one-off implementations.
- Favor additive shared infrastructure first, then migrate consumers in behavior-safe steps when a full replacement is too risky for a single change.
- When working in framework code meant for external adoption, optimize for consistency and long-term maintainability across providers and hosts, not just local fixes.
- Keep AI analytics ownership in the framework instead of sample hosts: shared usage/chat analytics services and contracts belong under `Abstractions`/`Primitives`, while YesSql and EntityCore provide the provider-specific stores, and framework features should not use `Sample*` naming.
- For optional provider integrations in sample hosts, do not eagerly read validated options in UI setup paths when an unconfigured provider should simply appear unavailable rather than crash the page.
- For catalog entry models, always provide an authoritative `CatalogEntryHandlerBase<T>` implementation that includes a `PopulateAsync` mapping path for every known property reachable from `JsonNode`/`JsonObject`, uses the shared JSON helper extensions instead of ad-hoc parsing where practical, sets create-time defaults (timestamps and current user/owner values when the model supports them) in `InitializedAsync`/`CreatingAsync`, and validates required fields in `ValidatingAsync`.
- For any `INameAwareModel` flow that has an authoritative catalog handler, validate duplicate names in the handler so users see a validation error early, but keep the store-level uniqueness enforcement as the final safeguard instead of moving that responsibility into managers.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using CrestApps.Core.AI.Models;

namespace CrestApps.Core.AI.Chat;

/// <summary>
/// Provides shared chat-session analytics operations for recording and querying
/// session lifecycle, performance, conversion, and feedback metrics.
/// </summary>
public interface IAIChatSessionEventService : IAIChatSessionAnalyticsRecorder, IAIChatSessionConversionGoalRecorder
{
/// <summary>
/// Records that a chat session has started.
/// </summary>
/// <param name="chatSession">The chat session.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task RecordSessionStartedAsync(
AIChatSession chatSession,
CancellationToken cancellationToken = default);

/// <summary>
/// Records the final analytics state for a chat session.
/// </summary>
/// <param name="chatSession">The chat session.</param>
/// <param name="promptCount">The total prompt count.</param>
/// <param name="isResolved">Whether the session was resolved.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task RecordSessionEndedAsync(
AIChatSession chatSession,
int promptCount,
bool isResolved,
CancellationToken cancellationToken = default);

/// <summary>
/// Records token usage for a chat session.
/// </summary>
/// <param name="sessionId">The chat session identifier.</param>
/// <param name="inputTokens">The number of input tokens.</param>
/// <param name="outputTokens">The number of output tokens.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task RecordCompletionUsageAsync(
string sessionId,
int inputTokens,
int outputTokens,
CancellationToken cancellationToken = default);

/// <summary>
/// Records response-latency data for a chat session.
/// </summary>
/// <param name="sessionId">The chat session identifier.</param>
/// <param name="responseLatencyMs">The response latency in milliseconds.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task RecordResponseLatencyAsync(
string sessionId,
double responseLatencyMs,
CancellationToken cancellationToken = default);

/// <summary>
/// Records user-rating totals for a chat session.
/// </summary>
/// <param name="sessionId">The chat session identifier.</param>
/// <param name="thumbsUpCount">The number of positive ratings.</param>
/// <param name="thumbsDownCount">The number of negative ratings.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task RecordUserRatingAsync(
string sessionId,
int thumbsUpCount,
int thumbsDownCount,
CancellationToken cancellationToken = default);

/// <summary>
/// Retrieves chat-session analytics records matching the optional profile and date filters.
/// </summary>
/// <param name="profileId">The optional profile identifier filter.</param>
/// <param name="startDateUtc">The inclusive UTC start date filter.</param>
/// <param name="endDateUtc">The inclusive UTC end date filter.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>The matching chat-session events ordered by session start descending.</returns>
Task<IReadOnlyList<AIChatSessionEvent>> GetAsync(
string profileId,
DateTime? startDateUtc,
DateTime? endDateUtc,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using CrestApps.Core.AI.Models;

namespace CrestApps.Core.AI.Chat;

/// <summary>
/// Persists chat-session analytics records for reporting and post-session analysis.
/// </summary>
public interface IAIChatSessionEventStore
{
/// <summary>
/// Finds a chat-session analytics record by session identifier.
/// </summary>
/// <param name="sessionId">The chat session identifier.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>The matching analytics record, or <see langword="null"/> if not found.</returns>
Task<AIChatSessionEvent> FindBySessionIdAsync(
string sessionId,
CancellationToken cancellationToken = default);

/// <summary>
/// Saves a chat-session analytics record.
/// </summary>
/// <param name="chatSessionEvent">The analytics record to save.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task SaveAsync(
AIChatSessionEvent chatSessionEvent,
CancellationToken cancellationToken = default);

/// <summary>
/// Retrieves chat-session analytics records matching the optional profile and date filters.
/// </summary>
/// <param name="profileId">The optional profile identifier filter.</param>
/// <param name="startDateUtc">The inclusive UTC start date filter.</param>
/// <param name="endDateUtc">The inclusive UTC end date filter.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>The matching chat-session events ordered by session start descending.</returns>
Task<IReadOnlyList<AIChatSessionEvent>> GetAsync(
string profileId,
DateTime? startDateUtc,
DateTime? endDateUtc,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using CrestApps.Core.AI.Models;

namespace CrestApps.Core.AI.Chat;

/// <summary>
/// Persists and queries extracted-data snapshot records for chat sessions.
/// </summary>
public interface IAIChatSessionExtractedDataStore
{
/// <summary>
/// Saves the extracted-data snapshot record, creating or updating the existing
/// record for the same chat session.
/// </summary>
/// <param name="record">The extracted-data snapshot record to save.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task SaveAsync(
AIChatSessionExtractedDataRecord record,
CancellationToken cancellationToken = default);

/// <summary>
/// Deletes the extracted-data snapshot record for the specified chat session.
/// </summary>
/// <param name="sessionId">The chat session identifier.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns><see langword="true"/> when a record was deleted; otherwise <see langword="false"/>.</returns>
Task<bool> DeleteAsync(
string sessionId,
CancellationToken cancellationToken = default);

/// <summary>
/// Retrieves extracted-data snapshot records for the specified AI profile and
/// optional date range.
/// </summary>
/// <param name="profileId">The AI profile identifier.</param>
/// <param name="startDateUtc">The inclusive UTC start date filter.</param>
/// <param name="endDateUtc">The inclusive UTC end date filter.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task<IReadOnlyList<AIChatSessionExtractedDataRecord>> GetAsync(
string profileId,
DateTime? startDateUtc,
DateTime? endDateUtc,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using CrestApps.Core.AI.Models;

namespace CrestApps.Core.AI.Completions;

/// <summary>
/// Provides shared AI completion usage tracking operations for recording and querying
/// provider usage across chat sessions and other completion flows.
/// </summary>
public interface IAICompletionUsageService : IAICompletionUsageObserver
{
/// <summary>
/// Retrieves usage records captured within the optional UTC date range.
/// </summary>
/// <param name="startDateUtc">The inclusive UTC start date filter.</param>
/// <param name="endDateUtc">The inclusive UTC end date filter.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>The matching usage records ordered by creation time descending.</returns>
Task<IReadOnlyList<AICompletionUsageRecord>> GetAsync(
DateTime? startDateUtc,
DateTime? endDateUtc,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using CrestApps.Core.AI.Models;

namespace CrestApps.Core.AI.Completions;

/// <summary>
/// Persists AI completion usage records for reporting, auditing, and analytics.
/// </summary>
public interface IAICompletionUsageStore
{
/// <summary>
/// Saves a usage record.
/// </summary>
/// <param name="record">The usage record to persist.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task SaveAsync(
AICompletionUsageRecord record,
CancellationToken cancellationToken = default);

/// <summary>
/// Retrieves usage records captured within the optional UTC date range.
/// </summary>
/// <param name="startDateUtc">The inclusive UTC start date filter.</param>
/// <param name="endDateUtc">The inclusive UTC end date filter.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>The matching usage records ordered by creation time descending.</returns>
Task<IReadOnlyList<AICompletionUsageRecord>> GetAsync(
DateTime? startDateUtc,
DateTime? endDateUtc,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ namespace CrestApps.Core.AI.Models;

/// <summary>
/// Represents a single chunk of an AI document passed to the vector indexing pipeline.
/// This model is used as the record in <see cref="OrchardCore.Indexing.BuildDocumentIndexContext"/>
/// when indexing document chunks via <see cref="OrchardCore.Indexing.IDocumentIndexHandler"/>.
/// This model carries the chunk record used during document indexing.
/// </summary>
public sealed class AIDocumentChunkContext
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Text.Json.Serialization;

namespace CrestApps.Core.AI.Models;

/// <summary>
Expand All @@ -23,8 +25,10 @@ public sealed class PostSessionResult
public PostSessionTaskResultStatus Status { get; set; }

/// <summary>
/// Gets or sets the error message if the task failed.
/// Gets or sets the current in-memory error message if the task failed.
/// Persisted troubleshooting details live in <see cref="AttemptHistory"/>.
/// </summary>
[JsonIgnore]
public string ErrorMessage { get; set; }

/// <summary>
Expand All @@ -33,7 +37,13 @@ public sealed class PostSessionResult
public int Attempts { get; set; }

/// <summary>
/// Gets or sets the UTC timestamp when this result was processed.
/// Gets or sets the history of failed or incomplete attempts for this task.
/// </summary>
public List<PostSessionTaskAttempt> AttemptHistory { get; set; } = [];

/// <summary>
/// Gets or sets the UTC timestamp when this task reached a terminal processed state.
/// This is only populated when the task succeeds or reaches a final failure state.
/// </summary>
public DateTime ProcessedAtUtc { get; set; }
public DateTime? ProcessedAtUtc { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace CrestApps.Core.AI.Models;

/// <summary>
/// Stores the outcome of one failed or incomplete post-session task attempt.
/// </summary>
public sealed class PostSessionTaskAttempt
{
/// <summary>
/// Gets or sets the 1-based attempt number for this task execution.
/// </summary>
public int AttemptNumber { get; set; }

/// <summary>
/// Gets or sets the task status that remained after this attempt completed.
/// </summary>
public PostSessionTaskResultStatus Status { get; set; }

/// <summary>
/// Gets or sets the persisted error message for this attempt.
/// </summary>
public string ErrorMessage { get; set; }

/// <summary>
/// Gets or sets the UTC timestamp when this attempt outcome was recorded.
/// </summary>
public DateTime RecordedAtUtc { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
namespace CrestApps.Core;

/// <summary>
/// Extension methods for <see cref="ExtensibleEntity"/> to provide dynamic property storage,
/// matching the patterns from OrchardCore.Entities.Entity.
/// Extension methods for <see cref="ExtensibleEntity"/> to provide dynamic property storage.
/// </summary>
public static class ExtensibleEntityExtensions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace CrestApps.Core;

/// <summary>
/// Extension methods for JSON types to replace OrchardCore's JSON helpers.
/// Extension methods for working with JSON types used throughout CrestApps Core.
/// </summary>
public static class JsonExtensions
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text.Json.Serialization;
using CrestApps.Core.Models;
using CrestApps.Core.Services;

Expand Down Expand Up @@ -44,6 +45,17 @@ public sealed class SearchIndexProfile : CatalogItem, IIndexProfileInfo, INameAw
/// </summary>
public string EmbeddingDeploymentName { get; set; }

/// <summary>
/// Gets or sets the legacy embedding deployment identifier.
/// </summary>
[Obsolete("Use EmbeddingDeploymentName instead. Retained for backward compatibility.")]
[JsonIgnore]
public string EmbeddingDeploymentId
{
get => EmbeddingDeploymentName;
set => EmbeddingDeploymentName = value;
}

/// <summary>
/// Gets or sets the date and time when this index profile was created.
/// </summary>
Expand All @@ -59,6 +71,15 @@ public sealed class SearchIndexProfile : CatalogItem, IIndexProfileInfo, INameAw
/// </summary>
public string Author { get; set; }

/// <summary>
/// Sets the legacy embedding deployment identifier during deserialization.
/// </summary>
[JsonPropertyName("EmbeddingDeploymentId")]
public string LegacyEmbeddingDeploymentId
{
set => EmbeddingDeploymentName = value;
}

string IIndexProfileInfo.IndexProfileId => ItemId;

/// <summary>
Expand Down
Loading
Loading