Skip to content

Commit 8da697a

Browse files
committed
Add support for instructions in markdown files with YAML front-matter
If instructions are long and the primary content that defines an agent, using TOML is still an inconvenience. For longer text, markdown is king. We add support for markdown instructions by just relying on YAML front-matter to provide the section data to go along with it. Example: ``` --- id: ai.agents.notes description: Provides free-form memory client: Grok options: modelid: grok-4-fast --- You organize and keep notes for the user ```
1 parent e076c8a commit 8da697a

21 files changed

+404
-27
lines changed

.netconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,6 @@
164164
sha = c0a15b3c5e42a6f5e73b8e43ad3a335d7d6f3787
165165
etag = fe8de7929a8ecdb631911233ae3c6bad034b26b9802e62c3521918207f6d4068
166166
weak
167+
[file "src/Agents/Extensions/YamlConfigurationStreamParser.cs"]
168+
url = https://github.com/andrewlock/NetEscapades.Configuration/blob/master/src/NetEscapades.Configuration.Yaml/YamlConfigurationStreamParser.cs
169+
weak

readme.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,34 @@ This can be used by leveraging [Tomlyn.Extensions.Configuration](https://www.nug
133133
> avoiding unnecessary tokens being used for indentation while allowing flexible
134134
> formatting in the config file.
135135
136+
For longer instructions, markdown format plus YAML front-matter can be used for better readability:
137+
138+
```yaml
139+
---
140+
id: ai.agents.notes
141+
description: Provides free-form memory
142+
client: grok
143+
options:
144+
modelid: grok-4-fast
145+
---
146+
You organize and keep notes for the user.
147+
# Some header
148+
More content
149+
150+
## Another header
151+
...
152+
```
153+
154+
Use the provided `AddInstructionsFile` extension method to load instructions from files as follows:
155+
156+
```csharp
157+
var host = new HostApplicationBuilder(args);
158+
host.Configuration.AddInstructionsFile("notes.md", optional: false, reloadOnChange: true);
159+
```
160+
161+
The `id` field in the front-matter is required and specifies the configuration section name, and
162+
all other fields are added as if they were specified under it in the configuration.
163+
136164
### Extensible AI Contexts
137165

138166
The Microsoft [agent framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview) allows extending

sample/Client/Client.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<ItemGroup>
99
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
1010
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
11+
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.10.0" />
1112
<PackageReference Include="Smith" Version="0.2.5" />
1213
<PackageReference Include="Spectre.Console" Version="0.52.0" />
1314
<PackageReference Include="Spectre.Console.Json" Version="0.52.0" />
@@ -22,6 +23,7 @@
2223
</ItemGroup>
2324

2425
<ItemGroup>
26+
<ProjectReference Include="..\..\src\Agents\Agents.csproj" />
2527
<ProjectReference Include="..\..\src\Extensions\Extensions.csproj" />
2628
<ProjectReference Include="..\..\src\Extensions.CodeAnalysis\Extensions.CodeAnalysis.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
2729
</ItemGroup>

sample/Client/Program.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
using System.Net.Http.Json;
2-
using System.Text.Json.Serialization;
32
using Devlooped.Extensions.AI.OpenAI;
43
using OpenTelemetry.Metrics;
54
using OpenTelemetry.Trace;
6-
using Spectre.Console;
75

86
var builder = App.CreateBuilder(args);
97
#if DEBUG
108
builder.Environment.EnvironmentName = Environments.Development;
119
#endif
1210

1311
builder.AddServiceDefaults();
14-
builder.Services.AddHttpClient();
12+
builder.Services.AddHttpClient()
13+
.ConfigureHttpClientDefaults(b => b.AddStandardResilienceHandler());
1514

1615
var app = builder.Build(async (IServiceProvider services, CancellationToken cancellation) =>
1716
{

sample/Client/appsettings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,12 @@
77
"Endpoint": "http://localhost:5117/notes/v1"
88
}
99
}
10+
},
11+
"Logging": {
12+
"LogLevel": {
13+
"Default": "Warning",
14+
"System.Net.Http.HttpClient": "Error",
15+
"Microsoft.Hosting": "Error"
16+
}
1017
}
1118
}

sample/Directory.Build.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
<ItemGroup>
1414
<None Update=".env" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="Never" />
15-
<Content Include="*.json;*.ini;*.toml" Exclude="@(Content)" CopyToOutputDirectory="PreserveNewest" />
15+
<Content Include="*.json;*.ini;*.toml;*.md" Exclude="@(Content)" CopyToOutputDirectory="PreserveNewest" />
1616
</ItemGroup>
1717

1818
<ItemGroup Condition="'$(IsAspireHost)' != 'true'">

sample/Server/appsettings.Development.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
"Logging": {
33
"LogLevel": {
44
"Default": "Information",
5-
"Microsoft.AspNetCore": "Warning"
5+
"Microsoft.AspNetCore": "Warning",
6+
"Microsoft.Hosting": "Warning",
7+
"System.Net.Http.HttpClient": "Warning"
68
}
79
}
810
}

sample/Server/appsettings.json

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,12 @@
77
},
88
"AllowedHosts": "*",
99
"AI": {
10-
"Agents": {
11-
"Notes": {
12-
"Name": "notes",
13-
"Description": "Provides free-form memory",
14-
"Instructions": "You organize and keep notes for the user, using JSON-LD",
15-
"Client": "Grok",
16-
"Options": {
17-
"ModelId": "grok-4"
18-
}
19-
}
20-
},
2110
"Clients": {
2211
"Grok": {
23-
"Endpoint": "https://api.grok.ai/v1",
12+
"Endpoint": "https://api.x.ai/v1",
2413
"ModelId": "grok-4-fast-non-reasoning"
2514
}
2615
}
27-
}
16+
},
17+
"OpenTelemetry:ConsoleExporter": true
2818
}

sample/Server/notes.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
id: ai.agents.notes
3+
description: Provides free-form memory
4+
client: Grok
5+
options:
6+
modelid: grok-4-fast
7+
---
8+
You organize and keep notes for the user, using JSON-LD

sample/ServiceDefaults.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
using DotNetEnv.Configuration;
1+
using Devlooped.Agents.AI;
2+
using DotNetEnv.Configuration;
23
using OpenTelemetry;
34
using OpenTelemetry.Metrics;
45
using OpenTelemetry.Resources;
56
using OpenTelemetry.Trace;
67
using Tomlyn.Extensions.Configuration;
78

89

10+
911
#if WEB
1012
using Microsoft.Extensions.Diagnostics.HealthChecks;
1113
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
@@ -54,7 +56,10 @@ public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder)
5456
|| httpContext.Request.Path.StartsWithSegments(AlivenessEndpointPath)));
5557
#endif
5658
tracing.AddHttpClientInstrumentation();
57-
tracing.AddConsoleExporter();
59+
60+
// Only add console exporter if explicitly enabled in configuration
61+
if (builder.Configuration.GetValue<bool>("OpenTelemetry:ConsoleExporter"))
62+
tracing.AddConsoleExporter();
5863
})
5964
.WithMetrics(metrics =>
6065
{
@@ -83,8 +88,12 @@ public static TBuilder ConfigureReload<TBuilder>(this TBuilder builder)
8388
{
8489
foreach (var toml in Directory.EnumerateFiles(AppContext.BaseDirectory, "*.toml", SearchOption.AllDirectories))
8590
builder.Configuration.AddTomlFile(toml, optional: false, reloadOnChange: true);
91+
8692
foreach (var json in Directory.EnumerateFiles(AppContext.BaseDirectory, "*.json", SearchOption.AllDirectories))
8793
builder.Configuration.AddJsonFile(json, optional: false, reloadOnChange: true);
94+
95+
foreach (var md in Directory.EnumerateFiles(AppContext.BaseDirectory, "*.md", SearchOption.AllDirectories))
96+
builder.Configuration.AddInstructionsFile(md, optional: false, reloadOnChange: true);
8897
}
8998
else
9099
{
@@ -95,11 +104,14 @@ public static TBuilder ConfigureReload<TBuilder>(this TBuilder builder)
95104
// Only use configs outside of bin/ and obj/ directories since we want reload to happen from source files not output files
96105
bool IsSource(string path) => !path.StartsWith(outDir) && !path.StartsWith(objDir);
97106

107+
foreach (var toml in Directory.EnumerateFiles(baseDir, "*.toml", SearchOption.AllDirectories).Where(IsSource))
108+
builder.Configuration.AddTomlFile(toml, optional: false, reloadOnChange: true);
109+
98110
foreach (var json in Directory.EnumerateFiles(baseDir, "*.json", SearchOption.AllDirectories).Where(IsSource))
99111
builder.Configuration.AddJsonFile(json, optional: false, reloadOnChange: true);
100112

101-
foreach (var toml in Directory.EnumerateFiles(baseDir, "*.toml", SearchOption.AllDirectories).Where(IsSource))
102-
builder.Configuration.AddTomlFile(toml, optional: false, reloadOnChange: true);
113+
foreach (var md in Directory.EnumerateFiles(baseDir, "*.md", SearchOption.AllDirectories).Where(IsSource))
114+
builder.Configuration.AddInstructionsFile(md, optional: false, reloadOnChange: true);
103115
}
104116

105117
return builder;

0 commit comments

Comments
 (0)