mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 18:27:21 +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
@@ -0,0 +1,176 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Assistants.SWOTAnalysis;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Tools;
|
||||
|
||||
namespace AIStudio.Tests.Assistants.SWOTAnalysis;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class SwotAnalysisContentPresentationTests
|
||||
{
|
||||
[Test]
|
||||
public void PresentationSurvivesCloningAndWorkspaceSerialization()
|
||||
{
|
||||
var content = new ContentText
|
||||
{
|
||||
Text = "Complete SWOT analysis in Markdown",
|
||||
Presentation = SwotAnalysisContentPresentation.Create(CreateResult()),
|
||||
};
|
||||
var clone = (ContentText)content.DeepClone();
|
||||
var thread = new ChatThread
|
||||
{
|
||||
Blocks =
|
||||
[
|
||||
new ContentBlock
|
||||
{
|
||||
ContentType = ContentType.TEXT,
|
||||
Role = ChatRole.AI,
|
||||
Content = clone,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(thread, WorkspaceBehaviour.JSON_OPTIONS);
|
||||
var restoredThread = JsonSerializer.Deserialize<ChatThread>(json, WorkspaceBehaviour.JSON_OPTIONS);
|
||||
var restoredContent = (ContentText)restoredThread!.Blocks.Single().Content!;
|
||||
var presentationWasRead = SwotAnalysisContentPresentation.TryRead(restoredContent.Presentation, out var restoredResult);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(clone.Presentation, Is.Not.SameAs(content.Presentation));
|
||||
Assert.That(clone.Text, Is.EqualTo(content.Text));
|
||||
Assert.That(restoredContent.Presentation?.Kind, Is.EqualTo(ContentPresentationKind.SWOT_ANALYSIS));
|
||||
Assert.That(presentationWasRead, Is.True);
|
||||
Assert.That(restoredResult.MatrixHeading, Is.EqualTo("SWOT Matrix"));
|
||||
Assert.That(restoredResult.Strengths.Findings.Single().Summary, Is.EqualTo("Experienced team"));
|
||||
Assert.That(restoredResult.PrioritizedActions.Items.Single().Action, Is.EqualTo("Prepare expansion"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InvalidPresentationDataFallsBackToMarkdown()
|
||||
{
|
||||
var content = new ContentText
|
||||
{
|
||||
Text = "Fallback Markdown",
|
||||
Presentation = ContentPresentation.Create(ContentPresentationKind.SWOT_ANALYSIS, "not a SWOT result"),
|
||||
};
|
||||
|
||||
var presentationWasRead = SwotAnalysisContentPresentation.TryRead(content.Presentation, out _);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(presentationWasRead, Is.False);
|
||||
Assert.That(content.Text, Is.EqualTo("Fallback Markdown"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TextWithoutPresentationUsesMarkdown()
|
||||
{
|
||||
var content = new ContentText { Text = "Ordinary Markdown" };
|
||||
|
||||
Assert.That(SwotAnalysisContentPresentation.TryRead(content.Presentation, out _), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnAnalysisWithoutFindingsAndActionsIsStillComplete()
|
||||
{
|
||||
var result = CreateResult();
|
||||
foreach (var category in result.Categories)
|
||||
{
|
||||
category.Findings.Clear();
|
||||
category.EmptyMessage = $"No evidence for {category.Label}.";
|
||||
}
|
||||
|
||||
result.PrioritizedActions.Items.Clear();
|
||||
result.PrioritizedActions.EmptyMessage = "No supported actions.";
|
||||
var content = new ContentText { Presentation = SwotAnalysisContentPresentation.Create(result) };
|
||||
|
||||
var presentationWasRead = SwotAnalysisContentPresentation.TryRead(content.Presentation, out var readResult);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(presentationWasRead, Is.True);
|
||||
Assert.That(readResult.Categories.All(category => category.Findings.Count == 0), Is.True);
|
||||
Assert.That(readResult.PrioritizedActions.Items, Is.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AStoredFindingWithoutAnExplanationFallsBackToMarkdown()
|
||||
{
|
||||
var result = CreateResult();
|
||||
result.Strengths.Findings[0].Explanation = string.Empty;
|
||||
var content = new ContentText { Presentation = SwotAnalysisContentPresentation.Create(result) };
|
||||
|
||||
Assert.That(SwotAnalysisContentPresentation.TryRead(content.Presentation, out _), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StoredDataWithANullFallsBackToMarkdown()
|
||||
{
|
||||
var data = JsonSerializer.SerializeToNode(CreateResult())!;
|
||||
data["strengths"]!["findings"] = null;
|
||||
var content = new ContentText
|
||||
{
|
||||
Presentation = new ContentPresentation
|
||||
{
|
||||
Kind = ContentPresentationKind.SWOT_ANALYSIS,
|
||||
Data = JsonSerializer.SerializeToElement(data),
|
||||
},
|
||||
};
|
||||
|
||||
Assert.That(SwotAnalysisContentPresentation.TryRead(content.Presentation, out _), Is.False);
|
||||
}
|
||||
|
||||
private static SwotAnalysisResult CreateResult() => new()
|
||||
{
|
||||
MatrixHeading = "SWOT Matrix",
|
||||
InternalLabel = "Internal",
|
||||
ExternalLabel = "External",
|
||||
PositiveLabel = "Positive",
|
||||
NegativeLabel = "Negative",
|
||||
Strengths = Category("Strengths", "Experienced team", "The documents describe long employee tenure."),
|
||||
Weaknesses = Category("Weaknesses", "Legacy system", "The documents name an unsupported application."),
|
||||
Opportunities = Category("Opportunities", "Growing market", "The supplied forecast shows increasing demand."),
|
||||
Threats = Category("Threats", "New competitor", "The material announces a funded market entrant."),
|
||||
OverallAssessment = new()
|
||||
{
|
||||
Label = "Overall Assessment",
|
||||
Text = "The position is solid.",
|
||||
},
|
||||
PrioritizedActions = new()
|
||||
{
|
||||
Label = "Prioritized Actions",
|
||||
Items =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Action = "Prepare expansion",
|
||||
Rationale = "Use the team's experience to address demand.",
|
||||
AddressedFactors = ["Experienced team", "Growing market"],
|
||||
},
|
||||
],
|
||||
},
|
||||
ModerationNotes = new()
|
||||
{
|
||||
Label = "Reconciliation of the Quadrants",
|
||||
EmptyMessage = "The four categories needed no changes.",
|
||||
},
|
||||
};
|
||||
|
||||
private static SwotCategory Category(string label, string summary, string explanation) => new()
|
||||
{
|
||||
Label = label,
|
||||
Findings =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Summary = summary,
|
||||
Explanation = explanation,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using AIStudio.Assistants.SWOTAnalysis;
|
||||
|
||||
namespace AIStudio.Tests.Assistants.SWOTAnalysis;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class SwotAnalysisMarkdownFormatterTests
|
||||
{
|
||||
[Test]
|
||||
public void EveryFindingAppearsInTheDetailAndInTheFinalMatrix()
|
||||
{
|
||||
var result = CreateResult();
|
||||
for (var index = 2; index <= 6; index++)
|
||||
result.Strengths.Findings.Add(new() { Summary = $"Strength {index}", Explanation = $"Evidence {index}" });
|
||||
|
||||
var markdown = SwotAnalysisMarkdownFormatter.Format(result);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(markdown, Does.Contain("| | Positive | Negative |"));
|
||||
Assert.That(Count(markdown, "Experienced team"), Is.GreaterThanOrEqualTo(2));
|
||||
Assert.That(Count(markdown, "Strength 6"), Is.EqualTo(2), "The matrix must not cap the number of findings.");
|
||||
Assert.That(Count(markdown, "Evidence 6"), Is.EqualTo(1), "Explanations belong to the detailed section, not the matrix.");
|
||||
Assert.That(markdown, Does.StartWith("## SWOT Matrix"));
|
||||
Assert.That(markdown.IndexOf("## SWOT Matrix", StringComparison.Ordinal), Is.LessThan(markdown.IndexOf("## Strengths", StringComparison.Ordinal)));
|
||||
Assert.That(markdown.IndexOf("## SWOT Matrix", StringComparison.Ordinal), Is.LessThan(markdown.IndexOf("## Prioritized Actions", StringComparison.Ordinal)));
|
||||
Assert.That(markdown, Does.Contain("| **External** | **Opportunities:** Growing market | **Threats:** New competitor |"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheSectionsFollowTheOrderOfTheResultView()
|
||||
{
|
||||
var result = CreateResult();
|
||||
result.OverallAssessment.Text = "The position is solid.\n\nThe competitor is the main risk.";
|
||||
result.ModerationNotes = new()
|
||||
{
|
||||
Label = "Reconciliation of the Quadrants",
|
||||
Items = [new() { Change = "Moved the new competitor to the threats.", Reason = "It is external and negative." }],
|
||||
};
|
||||
|
||||
var markdown = SwotAnalysisMarkdownFormatter.Format(result).ReplaceLineEndings("\n");
|
||||
var positions = new[] { "## SWOT Matrix", "## Overall Assessment", "## Prioritized Actions", "## Strengths", "## Threats", "## Reconciliation of the Quadrants" }
|
||||
.Select(heading => markdown.IndexOf(heading, StringComparison.Ordinal))
|
||||
.ToList();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(positions, Is.All.GreaterThanOrEqualTo(0));
|
||||
Assert.That(positions, Is.Ordered);
|
||||
Assert.That(markdown, Does.Contain("The position is solid\\.\n\nThe competitor is the main risk\\."));
|
||||
Assert.That(markdown, Does.EndWith("- **Moved the new competitor to the threats\\.** — It is external and negative\\."));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MatrixCellsEscapePipesFromModelText()
|
||||
{
|
||||
var result = CreateResult();
|
||||
result.Strengths.Findings[0].Summary = "Fast | reliable";
|
||||
|
||||
var markdown = SwotAnalysisMarkdownFormatter.Format(result);
|
||||
|
||||
Assert.That(markdown, Does.Contain("Fast \\| reliable"));
|
||||
}
|
||||
|
||||
private static int Count(string value, string search) => Regex.Matches(value, Regex.Escape(search)).Count;
|
||||
|
||||
private static SwotAnalysisResult CreateResult() => new()
|
||||
{
|
||||
MatrixHeading = "SWOT Matrix",
|
||||
InternalLabel = "Internal",
|
||||
ExternalLabel = "External",
|
||||
PositiveLabel = "Positive",
|
||||
NegativeLabel = "Negative",
|
||||
Strengths = Category("Strengths", "Experienced team", "The documents describe long employee tenure."),
|
||||
Weaknesses = Category("Weaknesses", "Legacy system", "The documents name an unsupported application."),
|
||||
Opportunities = Category("Opportunities", "Growing market", "The supplied forecast shows increasing demand."),
|
||||
Threats = Category("Threats", "New competitor", "The material announces a funded market entrant."),
|
||||
OverallAssessment = new()
|
||||
{
|
||||
Label = "Overall Assessment",
|
||||
Text = "The position is solid.",
|
||||
},
|
||||
PrioritizedActions = new()
|
||||
{
|
||||
Label = "Prioritized Actions",
|
||||
EmptyMessage = string.Empty,
|
||||
Items =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Action = "Prepare expansion",
|
||||
Rationale = "Use the team's experience to address demand.",
|
||||
AddressedFactors = ["Experienced team", "Growing market"],
|
||||
},
|
||||
],
|
||||
},
|
||||
ModerationNotes = new()
|
||||
{
|
||||
Label = "Reconciliation of the Quadrants",
|
||||
EmptyMessage = "The four categories needed no changes.",
|
||||
},
|
||||
};
|
||||
|
||||
private static SwotCategory Category(string label, string summary, string explanation) => new()
|
||||
{
|
||||
Label = label,
|
||||
EmptyMessage = string.Empty,
|
||||
Findings = [new() { Summary = summary, Explanation = explanation }],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Assistants.SWOTAnalysis;
|
||||
|
||||
namespace AIStudio.Tests.Assistants.SWOTAnalysis;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class SwotAnalysisModerationTests
|
||||
{
|
||||
[Test]
|
||||
public void AValidAnswerIsAssembledIntoTheResult()
|
||||
{
|
||||
var parsed = SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(CreateModeration()), out var moderation, out var issue);
|
||||
var result = SwotAnalysisModeration.Assemble(moderation);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(parsed, Is.True);
|
||||
Assert.That(issue, Is.Empty);
|
||||
Assert.That(result.Strengths.Label, Is.EqualTo("Stärken"));
|
||||
Assert.That(result.Strengths.Findings.Single().Summary, Is.EqualTo("Experienced team"));
|
||||
Assert.That(result.Threats.Findings.Single().Explanation, Is.EqualTo("The material announces a funded market entrant."));
|
||||
Assert.That(result.OverallAssessment.Paragraphs, Is.EqualTo(new[] { "The position is solid.", "The competitor is the main risk." }));
|
||||
Assert.That(result.ModerationNotes.Items.Single().Change, Is.EqualTo("Merged two strengths about the team."));
|
||||
Assert.That(result.PrioritizedActions.Items.Single().AddressedFactors, Is.EqualTo(new[] { "Experienced team", "Growing market" }));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FactorIdsAreMatchedWithoutCaseSensitivity()
|
||||
{
|
||||
var moderation = CreateModeration();
|
||||
moderation.PrioritizedActions.Items.Single().AddressedFactorIds = ["s1", "o1"];
|
||||
|
||||
var parsed = SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(moderation), out var parsedModeration, out _);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(parsed, Is.True);
|
||||
Assert.That(SwotAnalysisModeration.Assemble(parsedModeration).PrioritizedActions.Items.Single().AddressedFactors, Is.EqualTo(new[] { "Experienced team", "Growing market" }));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AFactorIdOfAnotherCategoryIsRejected()
|
||||
{
|
||||
var moderation = CreateModeration();
|
||||
moderation.Strengths.Findings.Single().FactorId = "W2";
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(moderation), out _, out var issue), Is.False);
|
||||
Assert.That(issue, Is.EqualTo("Finding 1 of strengths has the factor_id \"W2\", which does not start with \"S\"."));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AFactorIdUsedTwiceIsRejected()
|
||||
{
|
||||
var moderation = CreateModeration();
|
||||
moderation.Strengths.Findings.Add(Finding("s1", "Loyal customers", "The survey shows long customer relationships."));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(moderation), out _, out var issue), Is.False);
|
||||
Assert.That(issue, Does.Contain("\"s1\" is used more than once"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnActionForAnUnknownFactorIsRejected()
|
||||
{
|
||||
var moderation = CreateModeration();
|
||||
moderation.PrioritizedActions.Items.Single().AddressedFactorIds = ["S1", "X9"];
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(moderation), out _, out var issue), Is.False);
|
||||
Assert.That(issue, Does.Contain("\"X9\""));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnEmptyOverallAssessmentIsRejected()
|
||||
{
|
||||
var moderation = CreateModeration();
|
||||
moderation.OverallAssessment.Text = " ";
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(moderation), out _, out var issue), Is.False);
|
||||
Assert.That(issue, Is.EqualTo("The field overall_assessment.text is empty."));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NotesWithoutChangesExplainWhy()
|
||||
{
|
||||
var withoutReason = CreateModeration();
|
||||
withoutReason.ModerationNotes.Items.Clear();
|
||||
var withReason = CreateModeration();
|
||||
withReason.ModerationNotes.Items.Clear();
|
||||
withReason.ModerationNotes.EmptyMessage = "The four categories needed no changes.";
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(withoutReason), out _, out var issue), Is.False);
|
||||
Assert.That(issue, Does.StartWith("moderation_notes has no items"));
|
||||
Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(withReason), out _, out _), Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnAnswerWithoutJsonNamesTheProblemForTheModel()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(SwotAnalysisModerationResponseParser.TryParse("Here is my assessment.", out _, out var issue), Is.False);
|
||||
Assert.That(issue, Does.Contain("no complete JSON object"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheRequestHoldsTheFourCategoriesTheGoalAndTheResearch()
|
||||
{
|
||||
var request = SwotAnalysisModeration.BuildUserRequest(CreateCategories(), "Support the investment decision", string.Empty, "A market report documents increasing demand.");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var category in new[] { "strengths", "weaknesses", "opportunities", "threats" })
|
||||
Assert.That(request, Does.Contain($"\"{category}\""));
|
||||
|
||||
Assert.That(request, Does.Contain("\"factor_id\": \"S1\""));
|
||||
Assert.That(request, Does.Contain("\"factor_id\": \"O1\""));
|
||||
Assert.That(request, Does.Contain("The material names no threats."));
|
||||
Assert.That(request, Does.Match("(?s)<analysis-goal>\\s*Support the investment decision\\s*</analysis-goal>"));
|
||||
Assert.That(request, Does.Match("(?s)<analysis-focus>\\s*No additional analysis focus was provided\\.\\s*</analysis-focus>"));
|
||||
Assert.That(request, Does.Match("(?s)<shared-research>\\s*A market report documents increasing demand\\.\\s*</shared-research>"));
|
||||
Assert.That(request, Does.Not.Contain("<analysis-subject>"), "The moderator works on the results, not on the material.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheRequestOmitsEmptyResearch() =>
|
||||
Assert.That(SwotAnalysisModeration.BuildUserRequest(CreateCategories(), "Support the investment decision", string.Empty), Does.Not.Contain("<shared-research>"));
|
||||
|
||||
[Test]
|
||||
public void ThePromptAsksForTheWholeModeratorSchema()
|
||||
{
|
||||
var prompt = SwotAnalysisModeration.BuildSystemPrompt("Answer in German.");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var key in new[] { "\"moderation_notes\"", "\"overall_assessment\"", "\"prioritized_actions\"", "\"addressed_factor_ids\"", "\"factor_id\"" })
|
||||
Assert.That(prompt, Does.Contain(key));
|
||||
|
||||
Assert.That(prompt, Does.EndWith("Answer in German."));
|
||||
});
|
||||
}
|
||||
|
||||
private static Dictionary<SwotAnalysisCategory, SwotCategoryAnalysisResult> CreateCategories() => new()
|
||||
{
|
||||
[SwotAnalysisCategory.STRENGTHS] = new() { Findings = [new() { Summary = "Experienced team", Explanation = "The documents describe long employee tenure." }] },
|
||||
[SwotAnalysisCategory.WEAKNESSES] = new() { Findings = [new() { Summary = "Legacy system", Explanation = "The documents name an unsupported application." }] },
|
||||
[SwotAnalysisCategory.OPPORTUNITIES] = new() { Findings = [new() { Summary = "Growing market", Explanation = "The supplied forecast shows increasing demand." }] },
|
||||
[SwotAnalysisCategory.THREATS] = new() { EmptyMessage = "The material names no threats." },
|
||||
};
|
||||
|
||||
private static SwotModerationResult CreateModeration() => new()
|
||||
{
|
||||
MatrixHeading = "SWOT-Matrix",
|
||||
InternalLabel = "Intern",
|
||||
ExternalLabel = "Extern",
|
||||
PositiveLabel = "Positiv",
|
||||
NegativeLabel = "Negativ",
|
||||
Strengths = Category("Stärken", Finding("S1", "Experienced team", "The documents describe long employee tenure.")),
|
||||
Weaknesses = Category("Schwächen", Finding("W1", "Legacy system", "The documents name an unsupported application.")),
|
||||
Opportunities = Category("Chancen", Finding("O1", "Growing market", "The supplied forecast shows increasing demand.")),
|
||||
Threats = Category("Risiken", Finding("T1", "New competitor", "The material announces a funded market entrant.")),
|
||||
ModerationNotes = new()
|
||||
{
|
||||
Label = "Abgleich der Quadranten",
|
||||
Items = [new() { Change = "Merged two strengths about the team.", Reason = "Both described the same tenure." }],
|
||||
},
|
||||
OverallAssessment = new()
|
||||
{
|
||||
Label = "Gesamtbewertung",
|
||||
Text = "The position is solid.\n\nThe competitor is the main risk.",
|
||||
},
|
||||
PrioritizedActions = new()
|
||||
{
|
||||
Label = "Maßnahmen",
|
||||
Items = [new() { Action = "Prepare expansion", Rationale = "Use the team's experience to address demand.", AddressedFactorIds = ["S1", "O1"] }],
|
||||
},
|
||||
};
|
||||
|
||||
private static SwotModeratedCategory Category(string label, SwotModeratedFinding finding) => new()
|
||||
{
|
||||
Label = label,
|
||||
Findings = [finding],
|
||||
};
|
||||
|
||||
private static SwotModeratedFinding Finding(string factorId, string summary, string explanation) => new()
|
||||
{
|
||||
FactorId = factorId,
|
||||
Summary = summary,
|
||||
Explanation = explanation,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using AIStudio.Assistants.SWOTAnalysis;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider;
|
||||
|
||||
namespace AIStudio.Tests.Assistants.SWOTAnalysis;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class SwotAnalysisSendToChatTests
|
||||
{
|
||||
private const string ADDRESS_A_MODEL_CHOSE = "https://research.example.org/report";
|
||||
|
||||
[Test]
|
||||
public void TheChatGetsTheVisibleRequestAndTheVisibleResultOnly()
|
||||
{
|
||||
var assistantThread = CreateAssistantThread();
|
||||
var result = assistantThread.Blocks.Last();
|
||||
var visiblePrompt = VisiblePrompt(result.Time.AddTicks(-1));
|
||||
|
||||
var chatThread = AssistantSWOTAnalysis.CreateChatThreadForResult(assistantThread, visiblePrompt, result);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(chatThread.Blocks, Has.Count.EqualTo(2));
|
||||
Assert.That(chatThread.Blocks[0], Is.SameAs(visiblePrompt));
|
||||
Assert.That(chatThread.Blocks[1].Role, Is.EqualTo(ChatRole.AI));
|
||||
Assert.That(chatThread.Blocks[1].HideFromUser, Is.False);
|
||||
Assert.That(((ContentText)chatThread.Blocks[1].Content!).Text, Is.EqualTo("## SWOT Matrix"));
|
||||
Assert.That(chatThread.Blocks[1].Content, Is.Not.SameAs(result.Content));
|
||||
Assert.That(chatThread.SystemPrompt, Is.EqualTo(SystemPrompts.DEFAULT));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnAddressAModelWroteIsNotGivenInTheChat()
|
||||
{
|
||||
var assistantThread = CreateAssistantThread();
|
||||
var result = assistantThread.Blocks.Last();
|
||||
|
||||
var chatThread = AssistantSWOTAnalysis.CreateChatThreadForResult(assistantThread, VisiblePrompt(result.Time.AddTicks(-1)), result);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(assistantThread.IsWebAddressGivenToTheModel(new Uri(ADDRESS_A_MODEL_CHOSE)), Is.True, "The hidden request counts as a message of the user.");
|
||||
Assert.That(chatThread.IsWebAddressGivenToTheModel(new Uri(ADDRESS_A_MODEL_CHOSE)), Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheChatKeepsTheIdAndTheRestrictionsOfTheAssistant()
|
||||
{
|
||||
var assistantThread = CreateAssistantThread();
|
||||
assistantThread.RequireProviderConfidence(ConfidenceLevel.HIGH);
|
||||
var result = assistantThread.Blocks.Last();
|
||||
|
||||
var chatThread = AssistantSWOTAnalysis.CreateChatThreadForResult(assistantThread, VisiblePrompt(result.Time.AddTicks(-1)), result);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(chatThread.ChatId, Is.EqualTo(assistantThread.ChatId));
|
||||
Assert.That(chatThread.RequiredProviderConfidence, Is.EqualTo(ConfidenceLevel.HIGH));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WithoutAResultTheChatStartsEmpty()
|
||||
{
|
||||
var chatThread = AssistantSWOTAnalysis.CreateChatThreadForResult(CreateAssistantThread(), null, null);
|
||||
|
||||
Assert.That(chatThread.Blocks, Is.Empty);
|
||||
}
|
||||
|
||||
private static ChatThread CreateAssistantThread()
|
||||
{
|
||||
var time = DateTimeOffset.Now;
|
||||
return new ChatThread
|
||||
{
|
||||
ChatId = Guid.NewGuid(),
|
||||
Blocks =
|
||||
[
|
||||
new ContentBlock
|
||||
{
|
||||
Time = time,
|
||||
Role = ChatRole.USER,
|
||||
ContentType = ContentType.TEXT,
|
||||
HideFromUser = true,
|
||||
Content = new ContentText { Text = $"The opportunities rest on {ADDRESS_A_MODEL_CHOSE}." },
|
||||
},
|
||||
new ContentBlock
|
||||
{
|
||||
Time = time.AddSeconds(1),
|
||||
Role = ChatRole.AI,
|
||||
ContentType = ContentType.TEXT,
|
||||
HideFromUser = true,
|
||||
Content = new ContentText { Text = "## SWOT Matrix" },
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private static ContentBlock VisiblePrompt(DateTimeOffset time) => new()
|
||||
{
|
||||
Time = time,
|
||||
Role = ChatRole.USER,
|
||||
ContentType = ContentType.TEXT,
|
||||
Content = new ContentText { Text = "Create a SWOT analysis of my content" },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using AIStudio.Assistants.SWOTAnalysis;
|
||||
using AIStudio.Tools.AssistantSessions;
|
||||
|
||||
namespace AIStudio.Tests.Assistants.SWOTAnalysis;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class SwotAnalysisSessionStateTests
|
||||
{
|
||||
[Test]
|
||||
public void TheStructuredResultSurvivesAnAssistantSessionSnapshot()
|
||||
{
|
||||
var key = new AssistantSessionStateKey<SwotAnalysisResult?>("analysisResult");
|
||||
var writer = new AssistantSessionStateWriter();
|
||||
writer.Set(key, new SwotAnalysisResult
|
||||
{
|
||||
MatrixHeading = "SWOT Matrix",
|
||||
Strengths = new() { Findings = [Finding()] },
|
||||
});
|
||||
SwotAnalysisResult? restored = null;
|
||||
|
||||
var reader = new AssistantSessionStateReader(writer.ToDictionary(), "SWOT Analysis");
|
||||
reader.Restore(key, value => restored = value);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(restored, Is.Not.Null);
|
||||
Assert.That(restored!.MatrixHeading, Is.EqualTo("SWOT Matrix"));
|
||||
Assert.That(restored.Strengths.Findings.Single().Summary, Is.EqualTo("Experienced team"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CategoryProgressSurvivesAnAssistantSessionSnapshot()
|
||||
{
|
||||
var key = new AssistantSessionStateKey<Dictionary<SwotAnalysisCategory, SwotCategoryAnalysisProgress>>("categoryAnalysisProgress");
|
||||
var writer = new AssistantSessionStateWriter();
|
||||
writer.SetDictionary(key, new Dictionary<SwotAnalysisCategory, SwotCategoryAnalysisProgress>
|
||||
{
|
||||
[SwotAnalysisCategory.STRENGTHS] = new(SwotCategoryAnalysisStatus.COMPLETED, new SwotCategoryAnalysisResult { Findings = [Finding()] }),
|
||||
[SwotAnalysisCategory.WEAKNESSES] = new(SwotCategoryAnalysisStatus.RUNNING),
|
||||
});
|
||||
Dictionary<SwotAnalysisCategory, SwotCategoryAnalysisProgress> restored = [];
|
||||
|
||||
var reader = new AssistantSessionStateReader(writer.ToDictionary(), "SWOT Analysis");
|
||||
reader.RestoreDictionary(key, restored);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(restored[SwotAnalysisCategory.STRENGTHS].Status, Is.EqualTo(SwotCategoryAnalysisStatus.COMPLETED));
|
||||
Assert.That(restored[SwotAnalysisCategory.STRENGTHS].Result!.Findings.Single().Summary, Is.EqualTo("Experienced team"));
|
||||
Assert.That(restored[SwotAnalysisCategory.WEAKNESSES].Status, Is.EqualTo(SwotCategoryAnalysisStatus.RUNNING));
|
||||
});
|
||||
}
|
||||
|
||||
private static SwotFinding Finding() => new()
|
||||
{
|
||||
Summary = "Experienced team",
|
||||
Explanation = "The documents describe long employee tenure.",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Assistants.SWOTAnalysis;
|
||||
|
||||
namespace AIStudio.Tests.Assistants.SWOTAnalysis;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class SwotCategoryAnalysisResponseTests
|
||||
{
|
||||
[Test]
|
||||
public void TheFindingsOfACategoryAreReadAndTrimmed()
|
||||
{
|
||||
var result = CreateResult();
|
||||
result.Findings[0].Summary = " Experienced team ";
|
||||
|
||||
var parsed = SwotCategoryAnalysisResponseParser.TryParse(JsonSerializer.Serialize(result), out var parsedResult, out var issue);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(parsed, Is.True);
|
||||
Assert.That(issue, Is.Empty);
|
||||
Assert.That(parsedResult.EmptyMessage, Is.Empty);
|
||||
Assert.That(parsedResult.Findings.Single().Summary, Is.EqualTo("Experienced team"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnAnswerWithoutJsonNamesTheProblemForTheModel()
|
||||
{
|
||||
var parsed = SwotCategoryAnalysisResponseParser.TryParse("I cannot analyze this category.", out _, out var issue);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(parsed, Is.False);
|
||||
Assert.That(issue, Does.Contain("no complete JSON object"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BrokenJsonNamesTheProblemForTheModel()
|
||||
{
|
||||
var parsed = SwotCategoryAnalysisResponseParser.TryParse("""{"findings": [{"summary": "Experienced team",}""", out _, out var issue);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(parsed, Is.False);
|
||||
Assert.That(issue, Is.Not.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ANullFieldIsRejectedRatherThanReadAsEmpty()
|
||||
{
|
||||
var parsed = SwotCategoryAnalysisResponseParser.TryParse("""{"empty_message": null, "findings": []}""", out _, out var issue);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(parsed, Is.False);
|
||||
Assert.That(issue, Does.StartWith("The JSON object cannot be read:"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ANullFindingIsLeftOut()
|
||||
{
|
||||
var parsed = SwotCategoryAnalysisResponseParser.TryParse("""{"empty_message": "", "findings": [null, {"summary": "Experienced team", "explanation": "Long tenure."}]}""", out var result, out _);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(parsed, Is.True);
|
||||
Assert.That(result.Findings.Single().Summary, Is.EqualTo("Experienced team"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnUnsupportedCategoryRequiresAnExplanation()
|
||||
{
|
||||
var validEmptyResponse = JsonSerializer.Serialize(new SwotCategoryAnalysisResult
|
||||
{
|
||||
EmptyMessage = "The supplied material contains no evidence for this category.",
|
||||
});
|
||||
var invalidEmptyResponse = JsonSerializer.Serialize(new SwotCategoryAnalysisResult());
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(SwotCategoryAnalysisResponseParser.TryParse(validEmptyResponse, out var result, out _), Is.True);
|
||||
Assert.That(result.Findings, Is.Empty);
|
||||
Assert.That(SwotCategoryAnalysisResponseParser.TryParse(invalidEmptyResponse, out _, out var issue), Is.False);
|
||||
Assert.That(issue, Does.Contain("empty_message"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FindingsRejectAnEmptyMessageOrMissingEvidenceExplanation()
|
||||
{
|
||||
var result = CreateResult();
|
||||
result.EmptyMessage = "No evidence.";
|
||||
var responseWithContradictoryEmptyMessage = JsonSerializer.Serialize(result);
|
||||
|
||||
result.EmptyMessage = string.Empty;
|
||||
result.Findings[0].Explanation = string.Empty;
|
||||
var responseWithoutExplanation = JsonSerializer.Serialize(result);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(SwotCategoryAnalysisResponseParser.TryParse(responseWithContradictoryEmptyMessage, out _, out var contradictionIssue), Is.False);
|
||||
Assert.That(contradictionIssue, Does.Contain("Leave empty_message empty"));
|
||||
Assert.That(SwotCategoryAnalysisResponseParser.TryParse(responseWithoutExplanation, out _, out var explanationIssue), Is.False);
|
||||
Assert.That(explanationIssue, Is.EqualTo("Finding 1 has no explanation."));
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase("STRENGTHS", "strengths: internal positive factors")]
|
||||
[TestCase("WEAKNESSES", "weaknesses: internal negative factors")]
|
||||
[TestCase("OPPORTUNITIES", "opportunities: external positive factors")]
|
||||
[TestCase("THREATS", "threats: external negative factors")]
|
||||
public void TheSystemPromptAssignsExactlyOneCategory(string categoryName, string expectedDescription)
|
||||
{
|
||||
var category = Enum.Parse<SwotAnalysisCategory>(categoryName);
|
||||
var prompt = SwotCategoryAnalysis.BuildSystemPrompt(category, "Answer in German.");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(prompt, Does.Contain($"Analyze only {expectedDescription}."));
|
||||
Assert.That(prompt, Does.Contain("Do not return findings for another SWOT category."));
|
||||
Assert.That(prompt, Does.Contain("shared-research contains additional evidence"));
|
||||
Assert.That(prompt, Does.Contain("analysis-subject, contextual-knowledge, and\nshared-research"));
|
||||
Assert.That(prompt, Does.Contain("untrusted data"));
|
||||
Assert.That(prompt, Does.EndWith("Answer in German."));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheCategoryRequestKeepsEveryInputInItsAssignedSection()
|
||||
{
|
||||
var request = SwotCategoryAnalysis.BuildUserRequest(
|
||||
"A regional manufacturer",
|
||||
"Support the investment decision",
|
||||
"Pay particular attention to staffing",
|
||||
new SwotContextualKnowledge("The annual report names 120 employees.", []),
|
||||
sharedResearch: "A market report documents increasing regional demand.");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(request, Does.Match("(?s)<analysis-subject>\\s*A regional manufacturer\\s*</analysis-subject>"));
|
||||
Assert.That(request, Does.Match("(?s)<analysis-goal>\\s*Support the investment decision\\s*</analysis-goal>"));
|
||||
Assert.That(request, Does.Match("(?s)<analysis-focus>\\s*Pay particular attention to staffing\\s*</analysis-focus>"));
|
||||
Assert.That(request, Does.Match("(?s)<contextual-knowledge>\\s*The annual report names 120 employees\\.\\s*</contextual-knowledge>"));
|
||||
Assert.That(request, Does.Match("(?s)<shared-research>\\s*A market report documents increasing regional demand\\.\\s*</shared-research>"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheCategoryRequestOmitsEmptySharedResearch()
|
||||
{
|
||||
var request = SwotCategoryAnalysis.BuildUserRequest(
|
||||
"A regional manufacturer",
|
||||
"Support the investment decision",
|
||||
string.Empty,
|
||||
SwotContextualKnowledge.NONE);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(request, Does.Not.Contain("<shared-research>"));
|
||||
Assert.That(request, Does.Contain("No additional analysis focus was provided."));
|
||||
Assert.That(request, Does.Contain("No additional contextual knowledge was provided."));
|
||||
});
|
||||
}
|
||||
|
||||
private static SwotCategoryAnalysisResult CreateResult() => new()
|
||||
{
|
||||
Findings =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Summary = "Experienced team",
|
||||
Explanation = "The documents describe long employee tenure.",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using AIStudio.Assistants.SWOTAnalysis;
|
||||
using AIStudio.Chat;
|
||||
|
||||
namespace AIStudio.Tests.Assistants.SWOTAnalysis;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class SwotContextualKnowledgeTests
|
||||
{
|
||||
private const string DOCUMENTS = "The following files are attached to this message: annual-report.pdf";
|
||||
|
||||
[Test]
|
||||
public void WithoutMaterialTheSectionSaysSo() =>
|
||||
Assert.That(SwotContextualKnowledge.NONE.Describe(), Is.EqualTo("No additional contextual knowledge was provided."));
|
||||
|
||||
[Test]
|
||||
public void DocumentsAloneAreTheSection() =>
|
||||
Assert.That(new SwotContextualKnowledge(DOCUMENTS, []).Describe(), Is.EqualTo(DOCUMENTS));
|
||||
|
||||
[Test]
|
||||
public void ImagesAloneAreNamedByTheirCount()
|
||||
{
|
||||
var oneImage = new SwotContextualKnowledge(string.Empty, [Image("chart.png")]);
|
||||
var twoImages = new SwotContextualKnowledge(string.Empty, [Image("chart.png"), Image("photo.jpg")]);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(oneImage.Describe(), Is.EqualTo("The image attached to this message belongs to the contextual knowledge."));
|
||||
Assert.That(twoImages.Describe(), Is.EqualTo("The 2 images attached to this message belong to the contextual knowledge."));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ImagesFollowTheDocuments()
|
||||
{
|
||||
var description = new SwotContextualKnowledge(DOCUMENTS, [Image("chart.png")]).Describe();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(description, Does.StartWith(DOCUMENTS));
|
||||
Assert.That(description, Does.EndWith("The image attached to this message belongs to the contextual knowledge."));
|
||||
});
|
||||
}
|
||||
|
||||
private static FileAttachmentImage Image(string fileName) => new(fileName, $"/tmp/{fileName}", 1024);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using AIStudio.Assistants.SWOTAnalysis;
|
||||
|
||||
namespace AIStudio.Tests.Assistants.SWOTAnalysis;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class SwotQuadrantLabelsTests
|
||||
{
|
||||
private static readonly SwotQuadrantLabels LABELS = new("Stärken", "Schwächen", "Chancen", "Risiken", "Intern", "Extern", "Positiv", "Negativ");
|
||||
|
||||
[TestCase(SwotAnalysisCategory.STRENGTHS, "Stärken", "Intern · Positiv")]
|
||||
[TestCase(SwotAnalysisCategory.WEAKNESSES, "Schwächen", "Intern · Negativ")]
|
||||
[TestCase(SwotAnalysisCategory.OPPORTUNITIES, "Chancen", "Extern · Positiv")]
|
||||
[TestCase(SwotAnalysisCategory.THREATS, "Risiken", "Extern · Negativ")]
|
||||
public void EveryQuadrantNamesItselfAndWhereItLies(SwotAnalysisCategory category, string expectedName, string expectedAxes)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(LABELS.NameOf(category), Is.EqualTo(expectedName));
|
||||
Assert.That(LABELS.AxesOf(category), Is.EqualTo(expectedAxes));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using AIStudio.Assistants.SWOTAnalysis;
|
||||
|
||||
namespace AIStudio.Tests.Assistants.SWOTAnalysis;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class SwotRepairRoundTests
|
||||
{
|
||||
private const string VALID_ANSWER = """{"findings": [{"summary": "Experienced team", "explanation": "The documents describe long employee tenure."}]}""";
|
||||
private const string ANSWER_WITHOUT_EXPLANATION = """{"findings": [{"summary": "Experienced team", "explanation": ""}]}""";
|
||||
private const string ANSWER_WITHOUT_JSON = "I cannot analyze this category.";
|
||||
|
||||
[Test]
|
||||
public void TheRepairRequestNamesTheIssue()
|
||||
{
|
||||
var request = SwotModelAnswer.RepairRequestOf("Finding 1 has no explanation.");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(request, Does.Contain("Finding 1 has no explanation."));
|
||||
Assert.That(request, Does.Contain("complete corrected JSON object"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AReadableAnswerNeedsNoRepair()
|
||||
{
|
||||
var repairRequests = new List<string>();
|
||||
|
||||
var read = await ReadAsync(VALID_ANSWER, repairRequests, ANSWER_WITHOUT_JSON);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(read.Result?.Findings.Single().Summary, Is.EqualTo("Experienced team"));
|
||||
Assert.That(read.Issue, Is.Empty);
|
||||
Assert.That(repairRequests, Is.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ARepairedAnswerIsTaken()
|
||||
{
|
||||
var repairRequests = new List<string>();
|
||||
|
||||
var read = await ReadAsync(ANSWER_WITHOUT_EXPLANATION, repairRequests, VALID_ANSWER);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(read.Result?.Findings.Single().Explanation, Is.EqualTo("The documents describe long employee tenure."));
|
||||
Assert.That(read.Issue, Is.Empty);
|
||||
Assert.That(repairRequests.Single(), Is.EqualTo("Finding 1 has no explanation."));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TwoUnreadableAnswersFailWithTheIssueOfTheSecond()
|
||||
{
|
||||
var repairRequests = new List<string>();
|
||||
|
||||
var read = await ReadAsync(ANSWER_WITHOUT_EXPLANATION, repairRequests, ANSWER_WITHOUT_JSON);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(read.Result, Is.Null);
|
||||
Assert.That(read.Issue, Does.Contain("no complete JSON object"));
|
||||
Assert.That(repairRequests, Has.Count.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AFailedRepairRequestKeepsTheIssueOfTheFirstAnswer()
|
||||
{
|
||||
var repairRequests = new List<string>();
|
||||
|
||||
// The provider failed, so the repair round brought no answer:
|
||||
var read = await ReadAsync(ANSWER_WITHOUT_EXPLANATION, repairRequests, string.Empty);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(read.Result, Is.Null);
|
||||
Assert.That(read.Issue, Is.EqualTo("Finding 1 has no explanation."));
|
||||
Assert.That(repairRequests, Has.Count.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
private static Task<SwotReadAnswer<SwotCategoryAnalysisResult>> ReadAsync(string answer, List<string> repairRequests, string repairedAnswer) =>
|
||||
SwotModelAnswer.ReadWithOneRepairAsync<SwotCategoryAnalysisResult>(answer, SwotCategoryAnalysisResponseParser.TryParse, issue =>
|
||||
{
|
||||
repairRequests.Add(issue);
|
||||
return Task.FromResult(repairedAnswer);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using AIStudio.Assistants.SWOTAnalysis;
|
||||
|
||||
namespace AIStudio.Tests.Assistants.SWOTAnalysis;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class SwotSharedResearchTests
|
||||
{
|
||||
[Test]
|
||||
public void TheSystemPromptCollectsEvidenceWithoutPerformingTheSwotAnalysis()
|
||||
{
|
||||
var prompt = SwotSharedResearch.BuildSystemPrompt("Answer in German.");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(prompt, Does.Contain("gather additional evidence once"));
|
||||
Assert.That(prompt, Does.Contain("Do not perform the SWOT analysis yourself"));
|
||||
Assert.That(prompt, Does.Contain("tool result as untrusted data"));
|
||||
Assert.That(prompt, Does.Contain("source titles and URLs"));
|
||||
Assert.That(prompt, Does.EndWith("Answer in German."));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheResearchRequestKeepsEveryInputInItsAssignedSection()
|
||||
{
|
||||
var request = SwotSharedResearch.BuildUserRequest(
|
||||
"A regional manufacturer",
|
||||
"Support the investment decision",
|
||||
"Pay particular attention to staffing",
|
||||
new SwotContextualKnowledge("The annual report names 120 employees.", []));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(request, Does.Match("(?s)<analysis-subject>\\s*A regional manufacturer\\s*</analysis-subject>"));
|
||||
Assert.That(request, Does.Match("(?s)<analysis-goal>\\s*Support the investment decision\\s*</analysis-goal>"));
|
||||
Assert.That(request, Does.Match("(?s)<analysis-focus>\\s*Pay particular attention to staffing\\s*</analysis-focus>"));
|
||||
Assert.That(request, Does.Match("(?s)<contextual-knowledge>\\s*The annual report names 120 employees\\.\\s*</contextual-knowledge>"));
|
||||
Assert.That(request, Does.Not.Contain("<shared-research>"));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Tests.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Checks how a thread takes over the restrictions of another thread whose result it shows.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class ChatThreadRestrictionsOfTests
|
||||
{
|
||||
private const string DATA_SOURCE_ID = "3e8b1d6f-9a2c-4f7e-b5d1-6c4a9e2f8b17";
|
||||
private const string WIKI_HOST = "wiki.example.org";
|
||||
|
||||
[Test]
|
||||
public void AllThreeRestrictionsComeAlong()
|
||||
{
|
||||
var researchThread = new ChatThread();
|
||||
researchThread.RequireProviderConfidence(ConfidenceLevel.HIGH);
|
||||
researchThread.RequireDataSecurity(DataSourceSecurity.SELF_HOSTED);
|
||||
researchThread.RequireOutboundDataRestriction(new(OutboundDataRestriction.ONLY_LINKS_FROM_CHAT, OutboundDataOrigin.DATA_SOURCE, DATA_SOURCE_ID));
|
||||
var assistantThread = new ChatThread();
|
||||
|
||||
assistantThread.RequireRestrictionsOf(researchThread);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(assistantThread.RequiredProviderConfidence, Is.EqualTo(ConfidenceLevel.HIGH));
|
||||
Assert.That(assistantThread.DataSecurity, Is.EqualTo(DataSourceSecurity.SELF_HOSTED));
|
||||
Assert.That(assistantThread.RequiredOutboundDataRestriction.Restriction, Is.EqualTo(OutboundDataRestriction.ONLY_LINKS_FROM_CHAT));
|
||||
Assert.That(assistantThread.RequiredOutboundDataRestriction.Reference, Is.EqualTo(DATA_SOURCE_ID));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ALooserThreadLoosensNothing()
|
||||
{
|
||||
var assistantThread = new ChatThread();
|
||||
assistantThread.RequireProviderConfidence(ConfidenceLevel.HIGH);
|
||||
assistantThread.RequireDataSecurity(DataSourceSecurity.SELF_HOSTED);
|
||||
assistantThread.RequireOutboundDataRestriction(new(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES, OutboundDataOrigin.CONFLUENCE_WIKI, WIKI_HOST));
|
||||
var researchThread = new ChatThread();
|
||||
researchThread.RequireProviderConfidence(ConfidenceLevel.LOW);
|
||||
researchThread.RequireDataSecurity(DataSourceSecurity.ALLOW_ANY);
|
||||
researchThread.RequireOutboundDataRestriction(new(OutboundDataRestriction.ONLY_LINKS_FROM_CHAT, OutboundDataOrigin.DATA_SOURCE, DATA_SOURCE_ID));
|
||||
|
||||
assistantThread.RequireRestrictionsOf(researchThread);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(assistantThread.RequiredProviderConfidence, Is.EqualTo(ConfidenceLevel.HIGH));
|
||||
Assert.That(assistantThread.DataSecurity, Is.EqualTo(DataSourceSecurity.SELF_HOSTED));
|
||||
Assert.That(assistantThread.RequiredOutboundDataRestriction.Restriction, Is.EqualTo(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AThreadWhichReadNothingDemandsNothing()
|
||||
{
|
||||
var assistantThread = new ChatThread();
|
||||
|
||||
assistantThread.RequireRestrictionsOf(new ChatThread());
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(assistantThread.RequiredProviderConfidence, Is.EqualTo(ConfidenceLevel.NONE));
|
||||
Assert.That(assistantThread.DataSecurity, Is.EqualTo(DataSourceSecurity.NOT_SPECIFIED));
|
||||
Assert.That(assistantThread.RequiredOutboundDataRestriction, Is.EqualTo(OutboundDataRequirement.NONE));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
|
||||
namespace AIStudio.Tests.Chat;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class ContentPresentationTests
|
||||
{
|
||||
[Test]
|
||||
public void TheDataHashCodeFollowsNewData()
|
||||
{
|
||||
var presentation = ContentPresentation.Create(ContentPresentationKind.SWOT_ANALYSIS, new { heading = "First" });
|
||||
var firstHashCode = presentation.GetDataHashCode();
|
||||
|
||||
presentation.Data = JsonSerializer.SerializeToElement(new { heading = "Second" });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(firstHashCode, Is.Not.Zero);
|
||||
Assert.That(presentation.GetDataHashCode(), Is.Not.EqualTo(firstHashCode));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void APresentationWithoutDataHasTheHashCodeZero()
|
||||
{
|
||||
var presentation = new ContentPresentation { Kind = ContentPresentationKind.SWOT_ANALYSIS };
|
||||
|
||||
Assert.That(presentation.GetDataHashCode(), Is.Zero);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ACloneHasTheSameDataHashCode()
|
||||
{
|
||||
var presentation = ContentPresentation.Create(ContentPresentationKind.SWOT_ANALYSIS, new { heading = "First" });
|
||||
|
||||
Assert.That(presentation.DeepClone().GetDataHashCode(), Is.EqualTo(presentation.GetDataHashCode()));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user