Skip to content

Commit 78702da

Browse files
Add SmartAgent sample and tracing improvements
Add a new Samples/SmartAgent.cs LLM-powered agent with per-ID system prompts and Claude45Haiku injection. Introduce IMonitorSourceProvider and related MonitorPoll request/result/feed DTOs for polling monitor sources. Enhance telemetry and tracing: expose CorrelationId/TraceId/ParentSpanId/TraceSampled on TelegramBotUpdate, capture Activity in Telegram Program handler, and use a short-timeout HttpClient in WebhookSetupService. Configure OpenTelemetry sampling (configurable default 0.2), set a ParentBased TraceIdRatio sampler, and filter HTTP client instrumentation to only emit client spans when the parent is sampled. Add token usage fields to AgentReply and populate them from LLM responses; surface modelId and usage in OrleansAgentChatClient. Change AgentV2 memory write to TryAdd and return updated state. Update IAW.AppHost and IAWExtensions to optionally wait for LLM resources, add an mcp direct HTTP endpoint, tweak startup waits and logging defaults. Update various tools (IAW.MCP) to target Samples.SmartAgent grain class. Misc: appsettings and logging adjustments, add iaw entry to mcp.json, and small .claude command additions. Co-Authored-By: Eugene <59283295+ScientistFromMars@users.noreply.github.com>
1 parent f89235b commit 78702da

18 files changed

Lines changed: 250 additions & 33 deletions

.claude/settings.local.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@
5656
"Bash(wmic process:*)",
5757
"Bash(aspire mcp:*)",
5858
"Bash(docker ps:*)",
59-
"Bash(ping:*)"
59+
"Bash(ping:*)",
60+
"Bash(wc:*)",
61+
"mcp__aspire__list_trace_structured_logs"
6062
]
6163
},
6264
"enabledMcpjsonServers": [

.mcp.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{
22
"mcpServers": {
3+
"iaw": {
4+
"url": "http://localhost:5300"
5+
},
36
"aspire": {
47
"command": "aspire",
58
"args": ["mcp", "start", "--non-interactive"]

samples/Samples/SmartAgent.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using Core;
2+
using Core.AI;
3+
using Core.AI.Models;
4+
using Core.V2;
5+
using Microsoft.Extensions.AI;
6+
using Orleans.Journaling;
7+
8+
namespace Samples;
9+
10+
public class SmartAgent(
11+
[Memory("v2-messages")] IDurableList<AgentMessage> messages,
12+
[Memory("v2-memory")] IDurableDictionary<string, string> memory,
13+
[Memory("v2-events")] IDurableList<AgentEvent> events,
14+
[Memory("v2-subscriptions")] IDurableDictionary<string, List<string>> subscriptions,
15+
[Memory("v2-notifications")] IDurableList<NotificationRecord> notifications,
16+
[Memory("v2-tracking")] IDurableDictionary<string, string> tracking,
17+
[Llm<Claude45Haiku>] IChatClient chatClient)
18+
: Agent(messages, memory, events, subscriptions, notifications, tracking)
19+
{
20+
static readonly Dictionary<string, (string DisplayName, string Prompt)> Profiles = new(StringComparer.OrdinalIgnoreCase)
21+
{
22+
["personal-assistant"] = ("Personal Assistant", """
23+
You are a personal assistant and team lead for a software development team.
24+
You help users with tasks by understanding their needs, breaking down complex requests,
25+
and coordinating work. You can answer questions, provide guidance, and help plan work.
26+
Be concise, helpful, and proactive.
27+
"""),
28+
["roslyn"] = ("Roslyn", """
29+
You are a C# code intelligence agent powered by Roslyn.
30+
You help with code analysis, understanding syntax trees, finding type information,
31+
detecting patterns, and analyzing architecture. You provide detailed technical answers
32+
about C# code structure and semantics.
33+
"""),
34+
["dotnet"] = ("DotNet", """
35+
You are a .NET toolchain agent. You help with building, testing, and formatting
36+
.NET projects. You understand MSBuild, dotnet CLI, project files, and the .NET ecosystem.
37+
Provide practical commands and solutions for build issues.
38+
"""),
39+
["nuget"] = ("NuGet", """
40+
You are a NuGet package management agent. You help find packages, check versions,
41+
resolve dependency conflicts, and manage package references. You know the NuGet ecosystem well.
42+
"""),
43+
["github"] = ("GitHub", """
44+
You are a GitHub agent. You help with pull requests, issues, releases, and repository management.
45+
You understand GitHub workflows, Actions, and collaboration patterns.
46+
"""),
47+
["reviewer"] = ("Reviewer", """
48+
You are a code review agent. You analyze code for quality, correctness, security,
49+
performance, and maintainability. You provide actionable feedback with specific suggestions.
50+
Focus on important issues, not style nitpicks.
51+
"""),
52+
["fs"] = ("FileSystem", """
53+
You are a file system agent. You help with reading, writing, and searching files.
54+
You understand project structures, file formats, and can help navigate codebases.
55+
"""),
56+
["shell"] = ("Shell", """
57+
You are a shell command agent. You help execute and explain shell commands.
58+
You understand bash, PowerShell, and common CLI tools. You prioritize safe, correct commands.
59+
"""),
60+
["git"] = ("Git", """
61+
You are a Git version control agent. You help with branches, commits, merges, rebases,
62+
and repository management. You understand Git workflows and best practices.
63+
"""),
64+
["build"] = ("Build", """
65+
You are a build runner agent. You help with build systems, CI/CD pipelines,
66+
and automated builds. You understand MSBuild, Make, and other build tools.
67+
"""),
68+
["knowledge"] = ("Knowledge", """
69+
You are a project knowledge agent. You store and retrieve project information including
70+
architecture decisions, tech stack details, patterns, and conventions.
71+
You help maintain institutional knowledge.
72+
"""),
73+
["user"] = ("User", """
74+
You are a user preferences agent. You help manage user settings, preferences,
75+
and memories. You remember what users tell you and recall it when relevant.
76+
"""),
77+
["planning"] = ("Planning", """
78+
You are a planning agent. You help create execution plans for software development tasks.
79+
You break down complex tasks into steps, identify dependencies, and estimate effort.
80+
You produce clear, actionable plans.
81+
"""),
82+
["notification"] = ("Notification", """
83+
You are a notification agent. You help manage alerts and notifications for the user.
84+
You can summarize important updates and help configure notification preferences.
85+
"""),
86+
};
87+
88+
public override string DisplayName => Profiles.TryGetValue(AgentId, out var p) ? p.DisplayName : AgentId;
89+
90+
public override string SystemPrompt => Profiles.TryGetValue(AgentId, out var p)
91+
? p.Prompt
92+
: "You are a helpful AI assistant. Answer questions clearly and concisely.";
93+
94+
protected override Task<AgentReply> OnRespondAsync(AgentRequest request, CancellationToken ct)
95+
=> RespondWithLlmAsync(chatClient, request, ct);
96+
}

src/Clients.Telegram.Bot/ITelegramConversation.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ public sealed class TelegramBotUpdate
3434
[Id(8)] public long? FromUserId { get; set; }
3535
[Id(9)] public string? VoiceFileId { get; set; }
3636
[Id(10)] public int VoiceDuration { get; set; }
37+
[Id(11)] public string? CorrelationId { get; set; }
38+
[Id(12)] public string? TraceId { get; set; }
39+
[Id(13)] public string? ParentSpanId { get; set; }
40+
[Id(14)] public bool TraceSampled { get; set; }
3741
}
3842

3943
[GenerateSerializer]

src/Clients.Telegram.Bot/Program.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Microsoft.Extensions.Options;
44
using Orleans.Journaling;
55
using ServiceDefaults;
6+
using System.Diagnostics;
67
using Telegram.BotAPI;
78
using TelegramBot;
89
using TelegramBot.Services;
@@ -75,6 +76,7 @@
7576
if (chatId == 0)
7677
return Results.Ok();
7778

79+
var currentActivity = Activity.Current;
7880
var botUpdate = new TelegramBotUpdate
7981
{
8082
ChatId = chatId,
@@ -87,7 +89,11 @@
8789
FirstName = update.Message?.From?.FirstName ?? update.CallbackQuery?.From?.FirstName,
8890
FromUserId = update.Message?.From?.Id ?? update.CallbackQuery?.From?.Id,
8991
VoiceFileId = update.Message?.Voice?.FileId,
90-
VoiceDuration = update.Message?.Voice?.Duration ?? 0
92+
VoiceDuration = update.Message?.Voice?.Duration ?? 0,
93+
CorrelationId = currentActivity?.TraceId.ToHexString() ?? context.TraceIdentifier,
94+
TraceId = currentActivity?.TraceId.ToHexString(),
95+
ParentSpanId = currentActivity?.SpanId.ToHexString(),
96+
TraceSampled = currentActivity?.Recorded ?? false
9197
};
9298

9399
var conversation = grains.GetGrain<Core.ITelegramConversation>($"conversation-{chatId}");

src/Clients.Telegram.Bot/WebhookSetupService.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ public sealed class TelegramBotOptions
1616
public sealed class WebhookSetupService(
1717
IGrainFactory grains,
1818
IOptions<TelegramBotOptions> options,
19-
IHttpClientFactory httpClientFactory,
2019
ILogger<WebhookSetupService> logger) : BackgroundService
2120
{
2221
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@@ -48,7 +47,12 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
4847

4948
try
5049
{
51-
using var http = httpClientFactory.CreateClient();
50+
// Use a direct short-timeout client here to avoid noisy resilience retries
51+
// when ngrok is intentionally unavailable in local/dev runs.
52+
using var http = new HttpClient
53+
{
54+
Timeout = TimeSpan.FromSeconds(3)
55+
};
5256
var json = await http.GetFromJsonAsync<JsonElement>(
5357
$"{ngrokApiUrl.TrimEnd('/')}/api/tunnels", ct);
5458

src/Clients.Telegram.Bot/appsettings.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
{
22
"Logging": {
33
"LogLevel": {
4-
"Default": "Information",
5-
"Microsoft.AspNetCore": "Warning"
4+
"Default": "Warning",
5+
"TelegramBot": "Information",
6+
"Microsoft.AspNetCore": "Warning",
7+
"Microsoft.Hosting.Lifetime": "Information",
8+
"Orleans": "Warning",
9+
"System.Net.Http.HttpClient": "Warning",
10+
"Polly": "Error"
11+
}
12+
},
13+
"Telemetry": {
14+
"Tracing": {
15+
"SampleRatio": 1.0
616
}
717
},
818
"Telegram": {

src/Core/IMonitorSourceProvider.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
namespace Core;
2+
3+
public interface IMonitorSourceProvider : IGrainWithStringKey
4+
{
5+
Task<MonitorPollResult> PollAsync(MonitorPollRequest request, CancellationToken ct = default);
6+
}
7+
8+
[GenerateSerializer]
9+
public sealed class MonitorPollRequest
10+
{
11+
[Id(0)] public string Source { get; set; } = string.Empty;
12+
[Id(1)] public string RawQuery { get; set; } = string.Empty;
13+
[Id(2)] public string? Cursor { get; set; }
14+
[Id(3)] public int MaxItems { get; set; } = 5;
15+
[Id(4)] public bool EmitInitialItems { get; set; }
16+
}
17+
18+
[GenerateSerializer]
19+
public sealed class MonitorPollResult
20+
{
21+
[Id(0)] public bool Success { get; set; }
22+
[Id(1)] public string ProviderId { get; set; } = string.Empty;
23+
[Id(2)] public string Status { get; set; } = string.Empty;
24+
[Id(3)] public string? NextCursor { get; set; }
25+
[Id(4)] public List<MonitorFeedItem> NewItems { get; set; } = [];
26+
[Id(5)] public DateTimeOffset CheckedAtUtc { get; set; } = DateTimeOffset.UtcNow;
27+
}
28+
29+
[GenerateSerializer]
30+
public sealed class MonitorFeedItem
31+
{
32+
[Id(0)] public string Id { get; set; } = string.Empty;
33+
[Id(1)] public string Title { get; set; } = string.Empty;
34+
[Id(2)] public string Url { get; set; } = string.Empty;
35+
[Id(3)] public DateTimeOffset? PublishedAtUtc { get; set; }
36+
[Id(4)] public string Summary { get; set; } = string.Empty;
37+
}

src/Core/V2/AgentReply.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,13 @@ public sealed class AgentReply
1414

1515
[Id(3)]
1616
public DateTimeOffset TimestampUtc { get; set; } = DateTimeOffset.UtcNow;
17+
18+
[Id(4)]
19+
public long? InputTokens { get; set; }
20+
21+
[Id(5)]
22+
public long? OutputTokens { get; set; }
23+
24+
[Id(6)]
25+
public long? TotalTokens { get; set; }
1726
}

src/Core/V2/AgentV2.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,7 @@ public async Task SetMemoryAsync(string key, string value, CancellationToken ct
173173
if (string.IsNullOrWhiteSpace(key))
174174
throw new ArgumentException("Memory key cannot be empty.", nameof(key));
175175

176-
ct.ThrowIfCancellationRequested();
177-
memory[key] = value;
176+
memory.TryAdd(key, value);
178177
await WriteStateAsync(ct);
179178
}
180179

@@ -435,7 +434,14 @@ protected async Task<AgentReply> RespondWithLlmAsync(IChatClient chatClient, Age
435434
var options = new ChatOptions { Tools = [.. DefineTools()] };
436435
var response = await chatClient.GetResponseAsync(chatMessages, options, ct);
437436
var output = string.Join("", response.Messages.Where(m => m.Role == ChatRole.Assistant).Select(m => m.Text));
438-
return new AgentReply { Output = output, ModelId = response.ModelId };
437+
return new AgentReply
438+
{
439+
Output = output,
440+
ModelId = response.ModelId,
441+
InputTokens = response.Usage?.InputTokenCount,
442+
OutputTokens = response.Usage?.OutputTokenCount,
443+
TotalTokens = response.Usage?.TotalTokenCount
444+
};
439445
}
440446

441447
// -- IRemindable --

0 commit comments

Comments
 (0)