Skip to content

Instantly share code, notes, and snippets.

@pjmagee
Last active January 31, 2025 22:04
Show Gist options
  • Save pjmagee/9cc1b3a989681d79da435e8910337867 to your computer and use it in GitHub Desktop.
Save pjmagee/9cc1b3a989681d79da435e8910337867 to your computer and use it in GitHub Desktop.
Inspector / Suspect Simulation
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