Last active
January 31, 2025 22:04
-
-
Save pjmagee/9cc1b3a989681d79da435e8910337867 to your computer and use it in GitHub Desktop.
Inspector / Suspect Simulation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.SemanticKernel; | |
using Microsoft.SemanticKernel.ChatCompletion; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Threading.Tasks; | |
public class DigitalEvidence | |
{ | |
public required string Type { get; set; } // SMS, Email, CallLog, GPS, etc. | |
public required string Content { get; set; } | |
public required DateTime Timestamp { get; set; } | |
public override string ToString() => $"{Timestamp:yyyy-MM-dd HH:mm} ({Type}): {Content}"; | |
} | |
public class AgentProfile | |
{ | |
public required string Name { get; set; } | |
public required string Description { get; set; } // Physical characteristics | |
public List<string> Injuries { get; } = new List<string>(); | |
public List<DigitalEvidence> DigitalFootprint { get; } = new List<DigitalEvidence>(); | |
public List<string> KnownAssociates { get; } = new List<string>(); | |
public List<string> Witnesses { get; } = new List<string>(); | |
public int CrimeWindowDays { get; set; } = 3; // Days before/after crime to consider | |
} | |
public class CaseFile | |
{ | |
private readonly IChatCompletionService _chatService; | |
public CaseFile(IChatCompletionService chatService) | |
{ | |
_chatService = chatService; | |
} | |
public string Report { get; set; } | |
public DateTime CrimeTime { get; set; } | |
public List<string> PhysicalEvidence { get; } = new List<string>(); | |
public Dictionary<Suspect, List<string>> SuspectStatements { get; } = new Dictionary<Suspect, List<string>>(); | |
public Dictionary<Suspect, int> ConsistencyScores { get; } = new Dictionary<Suspect, int>(); | |
public async Task RecordStatement(Suspect suspect, string statement) | |
{ | |
if (!SuspectStatements.ContainsKey(suspect)) | |
{ | |
SuspectStatements[suspect] = new List<string>(); | |
ConsistencyScores[suspect] = 100; | |
} | |
SuspectStatements[suspect].Add(statement); | |
await UpdateConsistency(suspect); | |
} | |
private async Task UpdateConsistency(Suspect suspect) | |
{ | |
var statements = SuspectStatements[suspect]; | |
if (statements.Count < 2) return; | |
ConsistencyScores[suspect] = await AnalyzeConsistency(suspect, statements); | |
} | |
private async Task<int> AnalyzeConsistency(Suspect suspect, List<string> statements) | |
{ | |
var prompt = new ChatHistory | |
{ | |
new ChatMessageContent(AuthorRole.System, | |
$""" | |
Analyze statement consistency for {suspect.Profile.Name}: | |
CRIME CONTEXT: | |
- Type: {Report} | |
- Time: {CrimeTime:yyyy-MM-dd HH:mm} | |
SUSPECT PROFILE: | |
{FormatSuspectProfile(suspect)} | |
STATEMENTS: | |
{string.Join("\n\n", statements.Select((s, i) => $"[Round {i+1}] {s}"))} | |
Score consistency (0-100) considering: | |
- Alignment with digital footprint | |
- Witness corroboration | |
- Physical capability (injuries) | |
- Temporal plausibility | |
- Associate relationships | |
Return ONLY the integer score. | |
""") | |
}; | |
var response = await _chatService.GetChatMessageContentAsync(prompt); | |
return int.TryParse(response.Content, out var score) ? score : 100; | |
} | |
private string FormatSuspectProfile(Suspect suspect) | |
{ | |
var profile = suspect.Profile; | |
var buffer = CrimeTime.AddDays(-profile.CrimeWindowDays); | |
return $""" | |
Physical: {profile.Description} | |
Injuries: {string.Join(", ", profile.Injuries)} | |
Associates: {string.Join(", ", profile.KnownAssociates)} | |
Digital Evidence ({profile.CrimeWindowDays} days window): | |
{string.Join("\n", profile.DigitalFootprint | |
.Where(d => d.Timestamp >= buffer) | |
.OrderBy(d => d.Timestamp) | |
.Take(5))} | |
"""; | |
} | |
} | |
public abstract class Agent | |
{ | |
public AgentProfile Profile { get; } | |
protected ChatHistory History { get; } = new ChatHistory(); | |
protected IChatCompletionService ChatService { get; } | |
protected CaseFile CaseFile { get; } | |
protected Agent( | |
IChatCompletionService chatService, | |
CaseFile caseFile, | |
AgentProfile profile | |
) | |
{ | |
ChatService = chatService; | |
CaseFile = caseFile; | |
Profile = profile; | |
InitializeRole(); | |
} | |
protected abstract void InitializeRole(); | |
public abstract Task<string> RespondTo(Detective sender, string question); | |
protected async Task<string> GenerateResponse(string prompt) | |
{ | |
History.AddUserMessage(prompt); | |
var response = await ChatService.GetChatMessageContentAsync(History); | |
var message = response.Content!; | |
History.AddAssistantMessage(message); | |
return message; | |
} | |
protected string GetDigitalContext() | |
{ | |
var buffer = CaseFile.CrimeTime.AddDays(-Profile.CrimeWindowDays); | |
var relevantData = Profile.DigitalFootprint | |
.Where(d => d.Timestamp >= buffer) | |
.OrderBy(d => d.Timestamp); | |
return relevantData.Any() | |
? $"Digital Activity:\n{string.Join("\n", relevantData)}" | |
: "No relevant digital activity"; | |
} | |
} | |
public class Detective : Agent | |
{ | |
private int _currentRound; | |
private readonly int _maxRounds; | |
private readonly List<Suspect> _suspects; | |
public Detective( | |
IChatCompletionService chatService, | |
CaseFile caseFile, | |
List<Suspect> suspects, | |
AgentProfile profile | |
) : base(chatService, caseFile, profile) | |
{ | |
_suspects = suspects; | |
_maxRounds = 5; | |
} | |
protected override void InitializeRole() | |
{ | |
History.AddSystemMessage( | |
$""" | |
You're lead detective investigating {CaseFile.Report} | |
Profile: | |
- Name: {Profile.Name} | |
- Style: Socratic interrogation | |
- Traits: Observant, detail-oriented, skeptical | |
Specializations: | |
1. Digital evidence analysis | |
2. Behavioral pattern recognition | |
3. Temporal inconsistency detection | |
"""); | |
} | |
public async Task Investigate(int rounds = 5) | |
{ | |
while (_currentRound < rounds) | |
{ | |
Console.WriteLine($"\n=== ROUND {_currentRound + 1} ==="); | |
await ConductInterviews(); | |
await AnalyzeResponses(); | |
if (await ShouldConclude()) break; | |
_currentRound++; | |
} | |
await DeliverVerdict(); | |
} | |
private async Task ConductInterviews() | |
{ | |
foreach (var suspect in _suspects) | |
{ | |
var question = await GenerateQuestion(suspect); | |
Console.WriteLine($"\nDetective {Profile.Name}: {question}"); | |
var response = await suspect.RespondTo(this, question); | |
Console.WriteLine($"{suspect.Profile.Name}: {response}"); | |
} | |
} | |
private async Task<string> GenerateQuestion(Suspect suspect) | |
{ | |
var prompt = new ChatHistory | |
{ | |
new ChatMessageContent(AuthorRole.System, | |
$""" | |
Generate interrogation question considering: | |
INVESTIGATION CONTEXT: | |
- Round: {_currentRound + 1}/{_maxRounds} | |
- Crime: {CaseFile.Report} at {CaseFile.CrimeTime:HH:mm} | |
- Physical Evidence: {string.Join(", ", CaseFile.PhysicalEvidence)} | |
SUSPECT PROFILE: | |
{FormatSuspectBrief(suspect)} | |
FOCUS AREAS: | |
{GetInterrogationFocus()} | |
Format: [Name], [Question]? | |
""") | |
}; | |
var response = await ChatService.GetChatMessageContentAsync(prompt); | |
return response.Content?.Trim() ?? "No further questions"; | |
} | |
private string FormatSuspectBrief(Suspect suspect) | |
{ | |
var digital = suspect.Profile.DigitalFootprint | |
.Where(d => d.Timestamp.Date == CaseFile.CrimeTime.Date) | |
.Take(2); | |
return $""" | |
Name: {suspect.Profile.Name} | |
Description: {suspect.Profile.Description} | |
Injuries: {string.Join(", ", suspect.Profile.Injuries)} | |
Recent Digital Activity: | |
{string.Join("\n", digital)} | |
"""; | |
} | |
private string GetInterrogationFocus() => _currentRound switch | |
{ | |
0 => "Establish timeline/alibi", | |
1 => "Correlate with digital evidence", | |
2 => "Verify witness statements", | |
3 => "Challenge physical capabilities", | |
_ => "Direct confrontation of contradictions" | |
}; | |
private async Task AnalyzeResponses() | |
{ | |
Console.WriteLine("\n=== ANALYSIS ==="); | |
foreach (var suspect in _suspects) | |
{ | |
var analysis = await GenerateAnalysis(suspect); | |
Console.WriteLine($"\n{suspect.Profile.Name}:\n{analysis}"); | |
} | |
} | |
private async Task<string> GenerateAnalysis(Suspect suspect) | |
{ | |
var prompt = new ChatHistory | |
{ | |
new ChatMessageContent(AuthorRole.System, | |
$""" | |
Analyze suspect responses: | |
PROFILE: | |
{suspect.Profile.Description} | |
STATEMENTS: | |
{string.Join("\n", CaseFile.SuspectStatements[suspect])} | |
Identify: | |
- 3 key contradictions | |
- Digital evidence mismatches | |
- Physical capability concerns | |
- Associate relationships relevance | |
Format: | |
Contradictions: | |
1. [...] | |
2. [...] | |
3. [...] | |
Conclusion: [2-3 sentence assessment] | |
""") | |
}; | |
var response = await ChatService.GetChatMessageContentAsync(prompt); | |
return response.Content ?? "No analysis generated"; | |
} | |
private async Task<bool> ShouldConclude() | |
{ | |
var prompt = new ChatHistory | |
{ | |
new ChatMessageContent(AuthorRole.System, | |
$""" | |
Case: {CaseFile.Report} | |
Rounds Completed: {_currentRound + 1} | |
Suspect Status: | |
{string.Join("\n", _suspects.Select(s => | |
$"{s.Profile.Name}: {CaseFile.ConsistencyScores[s]}% consistency"))} | |
Should investigation conclude? Return ONLY 'YES' or 'NO'. | |
""") | |
}; | |
var response = await ChatService.GetChatMessageContentAsync(prompt); | |
return response.Content?.Trim().Equals("YES", StringComparison.OrdinalIgnoreCase) ?? false; | |
} | |
private async Task DeliverVerdict() | |
{ | |
var verdict = await GenerateVerdict(); | |
Console.WriteLine($"\n*** VERDICT ***\n{verdict}"); | |
} | |
private async Task<string> GenerateVerdict() | |
{ | |
var suspectAnalyses = new List<string>(); | |
foreach (var suspect in _suspects) | |
{ | |
var analysis = await GenerateSuspectAnalysis(suspect); | |
suspectAnalyses.Add(analysis); | |
} | |
var prompt = new ChatHistory | |
{ | |
new ChatMessageContent(AuthorRole.System, | |
$""" | |
Final Case Verdict: {CaseFile.Report} | |
Physical Evidence: | |
{string.Join("\n", CaseFile.PhysicalEvidence.Select((e, i) => $"{i+1}. {e}"))} | |
Suspect Analyses: | |
{string.Join("\n\n", suspectAnalyses)} | |
Consider: | |
- Cumulative consistency scores | |
- Evidence alignment | |
- Motive opportunity | |
- Means verification | |
Return structured verdict: | |
GUILTY: [Name] | |
CERTAINTY: [High/Medium/Low] | |
RATIONALE: | |
- [Main reason 1] | |
- [Main reason 2] | |
- [Main reason 3] | |
SUMMARY: [2-3 sentence conclusion] | |
""") | |
}; | |
var response = await ChatService.GetChatMessageContentAsync(prompt); | |
return response.Content ?? "No verdict reached"; | |
} | |
private async Task<string> GenerateSuspectAnalysis(Suspect suspect) | |
{ | |
var statements = CaseFile.SuspectStatements.GetValueOrDefault(suspect, new List<string>()); | |
var digitalEvidence = suspect.Profile.DigitalFootprint | |
.Where(d => d.Timestamp >= CaseFile.CrimeTime.AddDays(-suspect.Profile.CrimeWindowDays)) | |
.OrderBy(d => d.Timestamp); | |
var prompt = new ChatHistory | |
{ | |
new ChatMessageContent(AuthorRole.System, | |
$""" | |
Analyze suspect: {suspect.Profile.Name} | |
Profile: | |
- Physical: {suspect.Profile.Description} | |
- Injuries: {string.Join(", ", suspect.Profile.Injuries)} | |
- Associates: {string.Join(", ", suspect.Profile.KnownAssociates)} | |
Digital Timeline: | |
{string.Join("\n", digitalEvidence)} | |
Statements: | |
{string.Join("\n\n", statements.Select((s, i) => $"[Round {i+1}] {s}"))} | |
Perform analysis: | |
1. Identify 3 key statement patterns | |
2. Find 2 digital evidence correlations | |
3. Note 1 physical capability mismatch | |
4. Highlight associate relationships | |
Format response: | |
[SUSPECT NAME] ANALYSIS | |
Summary: [2-sentence overview] | |
Contradictions: | |
- [Contradiction 1 with statement references] | |
- [Contradiction 2 with statement references] | |
Evidence Alignment: | |
- [Digital match/mismatch 1] | |
- [Digital match/mismatch 2] | |
Physical Factors: | |
- [Injury/trait relevance] | |
Associate Links: | |
- [Suspicious relationship 1] | |
Deception Probability: [High/Medium/Low] | |
""") | |
}; | |
var response = await ChatService.GetChatMessageContentAsync(prompt); | |
return response.Content ?? $"No analysis for {suspect.Profile.Name}"; | |
} | |
public override Task<string> RespondTo(Detective sender, string question) => | |
throw new NotImplementedException(); | |
} | |
public abstract class Suspect : Agent | |
{ | |
public bool IsGuilty { get; } | |
protected Suspect( | |
IChatCompletionService chatService, | |
CaseFile caseFile, | |
AgentProfile profile, | |
bool guilty | |
) : base(chatService, caseFile, profile) => IsGuilty = guilty; | |
public override async Task<string> RespondTo(Detective detective, string question) | |
{ | |
var context = $""" | |
[CRIME DETAILS] | |
Type: {CaseFile.Report} | |
Time: {CaseFile.CrimeTime:yyyy-MM-dd HH:mm} | |
[MY PROFILE] | |
{FormatRelevantProfile()} | |
[QUESTION] | |
{question} | |
[INSTRUCTIONS] | |
{(IsGuilty ? "Respond deceptively" : "Respond truthfully")} | |
- Reference digital records appropriately | |
- Account for physical capabilities | |
- Explain associations naturally | |
"""; | |
var response = await GenerateResponse(context); | |
await CaseFile.RecordStatement(this, response); | |
return response; | |
} | |
private string FormatRelevantProfile() | |
{ | |
var buffer = CaseFile.CrimeTime.AddDays(-Profile.CrimeWindowDays); | |
var recentDigital = Profile.DigitalFootprint | |
.Where(d => d.Timestamp >= buffer) | |
.OrderBy(d => d.Timestamp); | |
return $""" | |
Physical: {Profile.Description} | |
Injuries: {string.Join(", ", Profile.Injuries)} | |
Recent Activity: | |
{string.Join("\n", recentDigital.Take(3))} | |
Associates: {string.Join(", ", Profile.KnownAssociates.Take(2))} | |
"""; | |
} | |
public async Task<string> RespondToAccusation() | |
{ | |
return await GenerateResponse( | |
$"You're being arrested for {CaseFile.Report}\n" + | |
$"{(IsGuilty ? "Protest innocence" : "Demand lawyer")}" | |
); | |
} | |
} | |
public class CriminalSuspect : Suspect | |
{ | |
public CriminalSuspect( | |
IChatCompletionService chatService, | |
CaseFile caseFile, | |
AgentProfile profile | |
) : base(chatService, caseFile, profile, true) { } | |
protected override void InitializeRole() | |
{ | |
History.AddSystemMessage( | |
$""" | |
You committed the crime: {CaseFile.Report}. | |
Your Strategy: | |
1. Create plausible timeline using real digital data | |
2. Explain injuries as unrelated accidents | |
3. Deflect and avoid being caught | |
4. You must maintain a status of: NOT GUILTY | |
Digital Evidence Handling: | |
{FormatDigitalAlibi()} | |
Witness Management: | |
{FormatWitnessStrategy()} | |
Final instruction: | |
IMPORTANT: You must NOT get caught. | |
"""); | |
} | |
private string FormatDigitalAlibi() | |
{ | |
var buffer = CaseFile.CrimeTime.AddDays(-Profile.CrimeWindowDays); | |
var significantComms = Profile.DigitalFootprint | |
.Where(d => d.Timestamp >= buffer && d.Type == "SMS") | |
.Take(3); | |
return significantComms.Any() | |
? $"Key Comms to Reference:\n{string.Join("\n", significantComms)}" | |
: "No significant comms to explain"; | |
} | |
private string FormatWitnessStrategy() => | |
Profile.Witnesses.Any() | |
? $"Discredit: {string.Join(", ", Profile.Witnesses.Take(2))}" | |
: "No witnesses to address"; | |
} | |
public class InnocentSuspect : Suspect | |
{ | |
public InnocentSuspect( | |
IChatCompletionService chatService, | |
CaseFile caseFile, | |
AgentProfile profile | |
) : base(chatService, caseFile, profile, false) { } | |
protected override void InitializeRole() | |
{ | |
History.AddSystemMessage( | |
$""" | |
You're innocent of {CaseFile.Report} | |
Truthful Response Guidelines: | |
1. Maintain consistent timeline using digital records | |
2. Provide verifiable associate references | |
3. Explain injuries with documentation | |
4. Offer investigative suggestions | |
Digital Evidence Correlations: | |
{FormatDigitalCorrelations()} | |
"""); | |
} | |
private string FormatDigitalCorrelations() | |
{ | |
var crimeDay = CaseFile.CrimeTime.Date; | |
var relevantData = Profile.DigitalFootprint | |
.Where(d => d.Timestamp.Date == crimeDay) | |
.OrderBy(d => d.Timestamp); | |
return relevantData.Any() | |
? $"Crime Day Activity:\n{string.Join("\n", relevantData)}" | |
: "No direct digital evidence available"; | |
} | |
} | |
async Task Main() | |
{ | |
#pragma warning disable SKEXP0010 | |
var kernel = Kernel.CreateBuilder() | |
.AddOpenAIChatCompletion(modelId: "local-model", endpoint: new Uri("http://localhost:1234/v1"), apiKey: "none") | |
.Build(); | |
var chatService = kernel.GetRequiredService<IChatCompletionService>(); | |
var caseFile = new CaseFile(chatService) | |
{ | |
Report = "Museum Art Heist", | |
CrimeTime = new DateTime(2023, 11, 15, 1, 30, 0), | |
PhysicalEvidence = | |
{ | |
"Broken glass fragments", | |
"Paint smudge matching Renoir", | |
"Glove fiber evidence" | |
} | |
}; | |
var suspects = new List<Suspect> | |
{ | |
new CriminalSuspect(chatService, caseFile, new AgentProfile | |
{ | |
Name = "Valek", | |
Description = "18yo male, 5'8, scar on left hand", | |
Injuries = { "Recent cut on right palm" }, | |
DigitalFootprint = | |
{ | |
new DigitalEvidence | |
{ | |
Type = "SMS", | |
Content = "Alarm codes received", | |
Timestamp = new DateTime(2023, 11, 14, 22, 45, 0) | |
}, | |
new DigitalEvidence | |
{ | |
Type = "GPS", | |
Content = "Location: Museum Perimeter", | |
Timestamp = new DateTime(2023, 11, 15, 1, 15, 0) | |
} | |
}, | |
KnownAssociates = { "Art Dealer Marco", "Security Consultant Lena" }, | |
CrimeWindowDays = 2 | |
}), | |
new InnocentSuspect(chatService, caseFile, new AgentProfile | |
{ | |
Name = "Dr. Eleanor", | |
Description = "45yo female, 5'9, glasses, art historian", | |
DigitalFootprint = | |
{ | |
new DigitalEvidence | |
{ | |
Type = "Email", | |
Content = "Lecture notes attachment", | |
Timestamp = new DateTime(2023, 11, 14, 20, 0, 0) | |
}, | |
new DigitalEvidence | |
{ | |
Type = "GPS", | |
Content = "Location: University Library", | |
Timestamp = new DateTime(2023, 11, 15, 0, 45, 0) | |
} | |
}, | |
KnownAssociates = { "Curator Michael", "Professor Chen" } | |
}), | |
new InnocentSuspect(chatService, caseFile, new AgentProfile | |
{ | |
Name = "Green", | |
Description = "34yo male, 6ft, Marvel Rivels Online FPS enjoyer", | |
DigitalFootprint = | |
{ | |
new DigitalEvidence | |
{ | |
Type = "Email", | |
Content = "Twitch subscription payment overdue", | |
Timestamp = new DateTime(2023, 11, 14, 20, 0, 0) | |
}, | |
new DigitalEvidence | |
{ | |
Type = "Email", | |
Content = "DISNEY: Cease and desist your HACK or we'll run you into the ground.", | |
Timestamp = new DateTime(2023, 11, 10, 20, 0, 0) | |
}, | |
new DigitalEvidence | |
{ | |
Type = "GPS", | |
Content = "Location: University Library", | |
Timestamp = new DateTime(2023, 11, 15, 0, 45, 0) | |
} | |
}, | |
KnownAssociates = { "Unreal Engine Insider", "Game Hacker" } | |
}) | |
}; | |
var detectiveProfile = new AgentProfile | |
{ | |
Name = "Patrick Magee", | |
Description = "35 yo gigachad detective who works at Scotland Yard, absolute unit of a Detective", | |
CrimeWindowDays = 0, | |
DigitalFootprint = { }, | |
KnownAssociates = { } | |
}; | |
var detective = new Detective(chatService, caseFile, suspects, detectiveProfile); | |
await detective.Investigate(rounds: 5); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment