mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 20:47:22 +00:00
Added a SWOT analysis assistant (#1122)
Co-authored-by: hart_s3 <sabrina.hartmann@dlr.de>
This commit is contained in:
1 parent
6cb20fd75a
commit
060a2a5734
87 files changed
+4601
-45
No files matched your search
@@ -176,6 +176,24 @@ public sealed record ChatThread
|
||||
/// <param name="requirement">What the data brought in demands.</param>
|
||||
public void RequireOutboundDataRestriction(OutboundDataRequirement requirement) => this.RequiredOutboundDataRestriction = this.RequiredOutboundDataRestriction.StricterOf(requirement);
|
||||
|
||||
/// <summary>
|
||||
/// Tightens this chat to everything another thread demands, because what that thread read comes
|
||||
/// along with its result.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For an assistant which lets tools work in a thread of its own and carries the result over into
|
||||
/// the thread it shows: without this, the result would arrive without the restrictions its data
|
||||
/// demands, and a chat started from it could hand that data to any provider or tool. All three
|
||||
/// ratchets go along, and each one only tightens.
|
||||
/// </remarks>
|
||||
/// <param name="otherThread">The thread whose result this chat takes over.</param>
|
||||
public void RequireRestrictionsOf(ChatThread otherThread)
|
||||
{
|
||||
this.RequireProviderConfidence(otherThread.RequiredProviderConfidence);
|
||||
this.RequireDataSecurity(otherThread.DataSecurity);
|
||||
this.RequireOutboundDataRestriction(otherThread.RequiredOutboundDataRestriction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
|
||||
/// </summary>
|
||||
|
||||
@@ -226,26 +226,28 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
var renderPlan = this.GetMarkdownRenderPlan(textContent.Text);
|
||||
<div @ref="this.mathContentContainer" class="chat-math-container">
|
||||
@foreach (var segment in renderPlan.Segments)
|
||||
{
|
||||
var segmentContent = segment.GetContent(renderPlan.Source);
|
||||
if (segment.Type is MarkdownRenderSegmentType.MARKDOWN)
|
||||
<ContentPresentationView Presentation="@textContent.Presentation">
|
||||
@{ var renderPlan = this.GetMarkdownRenderPlan(textContent.Text); }
|
||||
@foreach (var segment in renderPlan.Segments)
|
||||
{
|
||||
<ExportableMarkdown @key="@segment.RenderKey" Value="@segmentContent" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.CHAT_MARKDOWN_PIPELINE" Files="@this.FilesWithSaveButton" ValueStart="@segment.Start">
|
||||
<CodeBlockActions Context="file">
|
||||
<MudTooltip Text="@this.SaveAsLabel(file)" Placement="Placement.Top">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Save" Color="Color.Primary" Variant="Variant.Filled" Class="ma-2" OnClick="@(() => this.ExportFile(file))"/>
|
||||
</MudTooltip>
|
||||
</CodeBlockActions>
|
||||
</ExportableMarkdown>
|
||||
var segmentContent = segment.GetContent(renderPlan.Source);
|
||||
if (segment.Type is MarkdownRenderSegmentType.MARKDOWN)
|
||||
{
|
||||
<ExportableMarkdown @key="@segment.RenderKey" Value="@segmentContent" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.CHAT_MARKDOWN_PIPELINE" Files="@this.FilesWithSaveButton" ValueStart="@segment.Start">
|
||||
<CodeBlockActions Context="file">
|
||||
<MudTooltip Text="@this.SaveAsLabel(file)" Placement="Placement.Top">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Save" Color="Color.Primary" Variant="Variant.Filled" Class="ma-2" OnClick="@(() => this.ExportFile(file))"/>
|
||||
</MudTooltip>
|
||||
</CodeBlockActions>
|
||||
</ExportableMarkdown>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MathJaxBlock @key="@segment.RenderKey" Value="@segmentContent" Class="mb-5" />
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<MathJaxBlock @key="@segment.RenderKey" Value="@segmentContent" Class="mb-5" />
|
||||
}
|
||||
}
|
||||
</ContentPresentationView>
|
||||
@if (textContent.Sources.Count > 0)
|
||||
{
|
||||
<SourcesList @ref="this.sourcesList" Sources="@textContent.Sources" Passages="@textContent.SourcePassages"/>
|
||||
|
||||
@@ -430,6 +430,8 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
hash.Add(text.Sources.Count);
|
||||
hash.Add(text.SourcePassages.Count);
|
||||
hash.Add(text.ToolInvocations.Count);
|
||||
hash.Add(text.Presentation?.Kind);
|
||||
hash.Add(text.Presentation?.GetDataHashCode() ?? 0);
|
||||
hash.Add(text.ToolRuntimeStatus.IsRunning);
|
||||
hash.Add(text.ToolRuntimeStatus.Message);
|
||||
hash.Add(this.showToolTrace);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Structured data which a renderer may use instead of the canonical text of a message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The text stays canonical: providers, copying, exporting, and every renderer which does not know
|
||||
/// the presentation use it. ContentPresentationView shows the presentation instead, and falls back to
|
||||
/// the text when its data cannot be read, e.g., from a chat file which was changed by hand.
|
||||
/// </remarks>
|
||||
public sealed class ContentPresentation
|
||||
{
|
||||
private JsonElement? data;
|
||||
private int? dataHashCode;
|
||||
|
||||
/// <summary>
|
||||
/// The renderer which understands the data.
|
||||
/// </summary>
|
||||
public ContentPresentationKind Kind { get; set; } = ContentPresentationKind.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The renderer-specific data.
|
||||
/// </summary>
|
||||
public JsonElement? Data
|
||||
{
|
||||
get => this.data;
|
||||
set
|
||||
{
|
||||
this.data = value;
|
||||
this.dataHashCode = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static ContentPresentation Create<T>(ContentPresentationKind kind, T data) => new()
|
||||
{
|
||||
Kind = kind,
|
||||
Data = JsonSerializer.SerializeToElement(data),
|
||||
};
|
||||
|
||||
internal ContentPresentation DeepClone() => new()
|
||||
{
|
||||
Kind = this.Kind,
|
||||
Data = this.Data?.Clone(),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A hash code of the data, for telling whether it changed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Computed once per data, since every render of a message asks for it and computing means
|
||||
/// writing out the whole data.
|
||||
/// </remarks>
|
||||
internal int GetDataHashCode() => this.dataHashCode ??= this.Data is not { ValueKind: not JsonValueKind.Undefined } value
|
||||
? 0
|
||||
: value.GetRawText().GetHashCode(StringComparison.Ordinal);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies an optional specialized presentation of text content.
|
||||
/// </summary>
|
||||
public enum ContentPresentationKind
|
||||
{
|
||||
NONE,
|
||||
SWOT_ANALYSIS,
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@namespace AIStudio.Chat
|
||||
@using AIStudio.Assistants.SWOTAnalysis
|
||||
|
||||
@switch (this.data)
|
||||
{
|
||||
case SwotAnalysisResult swotAnalysisResult:
|
||||
<SwotAnalysisResultView Result="@swotAnalysisResult"/>
|
||||
break;
|
||||
|
||||
default:
|
||||
@this.ChildContent
|
||||
break;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using AIStudio.Assistants.SWOTAnalysis;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Shows the structured presentation of a text, or the text itself when it has none.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The one place which knows every kind of presentation and the view which shows it. Chats and
|
||||
/// assistants render each text through this component, so a new kind needs a case here and in
|
||||
/// Read, and nothing else in the chat.
|
||||
/// </remarks>
|
||||
public partial class ContentPresentationView
|
||||
{
|
||||
/// <summary>
|
||||
/// The presentation of the text, if it has one.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public ContentPresentation? Presentation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How the text looks without a presentation, and when its data cannot be read.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
private ContentPresentation? readPresentation;
|
||||
private int readDataHashCode;
|
||||
private object? data;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
//
|
||||
// Reading the data means deserializing it, so only a new presentation or changed data are
|
||||
// read again; a parent renders far more often than either happens:
|
||||
//
|
||||
var dataHashCode = this.Presentation?.GetDataHashCode() ?? 0;
|
||||
if (ReferenceEquals(this.Presentation, this.readPresentation) && dataHashCode == this.readDataHashCode)
|
||||
return;
|
||||
|
||||
this.readPresentation = this.Presentation;
|
||||
this.readDataHashCode = dataHashCode;
|
||||
this.data = Read(this.Presentation);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Reads the data of a presentation into the type its view shows.
|
||||
/// </summary>
|
||||
/// <param name="presentation">The presentation to read.</param>
|
||||
/// <returns>The data, or null when there is no presentation or its data cannot be read.</returns>
|
||||
private static object? Read(ContentPresentation? presentation) => presentation?.Kind switch
|
||||
{
|
||||
ContentPresentationKind.SWOT_ANALYSIS => SwotAnalysisContentPresentation.TryRead(presentation, out var result) ? result : null,
|
||||
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -57,6 +57,16 @@ public sealed class ContentText : IContent
|
||||
|
||||
public List<ToolInvocationTrace> ToolInvocations { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Optional structured data for a specialized visual presentation of this text.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The text remains the canonical content sent to providers, copied, and exported. A renderer
|
||||
/// which understands the presentation may use its data instead, while every other consumer
|
||||
/// safely continues to use the text.
|
||||
/// </remarks>
|
||||
public ContentPresentation? Presentation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The passages the data sources handed to the AI for this answer, as the AI received them.
|
||||
/// </summary>
|
||||
@@ -540,6 +550,7 @@ public sealed class ContentText : IContent
|
||||
Sources = [..this.Sources],
|
||||
SourcePassages = [..this.SourcePassages],
|
||||
FileAttachments = [..this.FileAttachments],
|
||||
Presentation = this.Presentation?.DeepClone(),
|
||||
ToolInvocations = [..this.ToolInvocations.Select(x => new ToolInvocationTrace
|
||||
{
|
||||
Order = x.Order,
|
||||
|
||||
Reference in new issue
Block a user