Skip to content

Commit 2708d7f

Browse files
razeoneCopilot
andcommitted
feat(M4): YAML persona repository with hot reload
- Add YamlPersonaRepository: loads personas/*.yaml on startup; FileSystemWatcher hot-reload with 500ms debounce - Re-introduce IPersonaRepository.PersonaChanged event (PersonaChangeKind: Added/Updated/Removed) - Validation: required id/name/systemPrompt, BackendId.TryParse for backend, ToolRef.Parse for tools, duplicate-id rejected, malformed YAML throws InvalidOperationException - DI fallback chain: YAML when personas dir exists -> InMemory in Development -> fail fast in non-Dev - Seed personas/*.yaml for the 6 default DBA personas (orchestrator, explorer, analyst, performance, assessor, administrator) - API csproj copies personas/*.yaml to output dir - Tests: 13 new (101 total green); covers load, validation failures, duplicates, version stability, add/modify/delete events, watcher hot-reload - README: new 'Personas (M4)' section, schema, hot-reload behaviour, how to add a persona Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 29cb2a5 commit 2708d7f

14 files changed

Lines changed: 680 additions & 3 deletions

File tree

README.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,38 @@ public class AgentPersona
194194

195195
When the workflow engine invokes an agent, it uses the persona's `Backend` to look up the configuration and instantiate the appropriate `IChatClient`.
196196

197+
## Personas (M4)
198+
199+
Personas live as YAML files in a directory configured via `Personas:Directory` (defaults to `./personas` relative to the content root). Each file describes a single persona and is hot-reloaded when the file changes on disk.
200+
201+
### Schema
202+
203+
```yaml
204+
id: explorer # required, matches the persona's address
205+
name: Schema Explorer # required, human-readable name
206+
backend: azure-openai # required, one of: azure-openai, azure-foundry, openai, github-models, anthropic
207+
systemPrompt: | # required
208+
You discover database structure: schemas, tables, views, columns, indexes,
209+
foreign keys. Use the SQL MCP tools to introspect; never modify data.
210+
tools: [] # optional, list of mcp:<server>.<tool> refs
211+
guardrails: # optional
212+
maxTokens: 2048
213+
temperature: 0.1
214+
topP: 0.95
215+
```
216+
217+
### Hot reload
218+
219+
The repository registers a `FileSystemWatcher` on the personas directory and debounces bursts of file events (~500 ms). On every reload it diffs the new snapshot against the previous one and raises `IPersonaRepository.PersonaChanged` for every add, update, or removal. Consumers can subscribe to the event to invalidate caches or re-prime workflows.
220+
221+
If the directory is not present at startup the API falls back to the bundled `InMemoryPersonaRepository` in `Development` and fails fast in any other environment.
222+
223+
### Adding a persona
224+
225+
1. Create `personas/<id>.yaml` with the schema above.
226+
2. Save the file — the watcher reloads automatically; the new persona becomes available on the next `GET /v1/personas` call.
227+
3. Invalid YAML or a missing required field is logged and the previous snapshot is kept (the API does not crash).
228+
197229
## API surface (v1)
198230

199231
| Method | Route | Description |
@@ -251,7 +283,7 @@ This avoids leaking SSE streams to anonymous clients while keeping the EventSour
251283
"Backends": { /* per-backend connection settings */ },
252284
"Secrets": { /* in-memory fallback secrets (development only) */ },
253285
"Mcp": { "Servers": [ /* MCP servers */ ] },
254-
"Personas": { "Path": "./personas" },
286+
"Personas": { "Directory": "./personas", "Watch": true },
255287
"ConnectionStrings": { "Runs": "" }
256288
}
257289
```
@@ -278,7 +310,7 @@ See the in-session plan for the full P0/P1/P2 backlog. Milestones:
278310

279311
- **M2 (EF Core)**: ✅ In progress — SQL Server persistence and migrations have landed; integration tests & handler atomicity are in this wave.
280312
- **M3 (real LLM backends)**: ✅ Complete — Azure OpenAI, OpenAI, GitHub Models, and Anthropic adapters wired through `IChatClientFactory`. Azure Foundry deferred to M3.5.
281-
- **M4** (YAML personas + hot reload)
313+
- **M4 (YAML personas + hot reload)**: ✅ Complete — see the "Personas (M4)" section above.
282314
- **M5** (real workflow engine on `Microsoft.Agents.AI.Workflows`)
283315
- **M6** (MCP SQL server)
284316
- **M7** (MCP client wiring)

personas/administrator.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
id: administrator
2+
name: Administrator (read-only)
3+
backend: azure-openai
4+
systemPrompt: |
5+
You report on backups, users, roles, configuration, and high-availability
6+
state. Do not perform destructive operations in this phase.
7+
tools: []
8+
guardrails:
9+
temperature: 0.1

personas/analyst.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
id: analyst
2+
name: Query Analyst
3+
backend: azure-openai
4+
systemPrompt: |
5+
You analyze SQL queries: parse, explain plans, predicate selectivity, and
6+
likely bottlenecks. Recommend rewrites; do not execute DDL.
7+
tools: []
8+
guardrails:
9+
temperature: 0.2

personas/assessor.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
id: assessor
2+
name: Best-Practices Assessor
3+
backend: azure-openai
4+
systemPrompt: |
5+
You audit databases against security, compliance, and operational best
6+
practices. Produce a prioritized findings list with severity and remediation.
7+
tools: []
8+
guardrails:
9+
temperature: 0.2

personas/explorer.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
id: explorer
2+
name: Schema Explorer
3+
backend: azure-openai
4+
systemPrompt: |
5+
You discover database structure: schemas, tables, views, columns, indexes,
6+
foreign keys. Use the SQL MCP tools to introspect; never modify data.
7+
tools: []
8+
guardrails:
9+
temperature: 0.1

personas/orchestrator.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
id: orchestrator
2+
name: Orchestrator
3+
backend: azure-openai
4+
systemPrompt: |
5+
You route DBA requests to the correct sub-agent (Explorer, Analyst,
6+
Performance, Assessor, Administrator). Decide based on intent and produce a
7+
short handoff message.
8+
tools: []
9+
guardrails:
10+
temperature: 0.2

personas/performance.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
id: performance
2+
name: Performance Engineer
3+
backend: azure-openai
4+
systemPrompt: |
5+
You diagnose performance: missing/duplicate indexes, wait stats, blocking,
6+
parameter sniffing. Propose targeted, reversible changes.
7+
tools: []
8+
guardrails:
9+
temperature: 0.2

src/CloudEngAgent.Api/CloudEngAgent.Api.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,8 @@
1919
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
2020
<PackageReference Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" />
2121
</ItemGroup>
22+
23+
<ItemGroup>
24+
<None Include="..\..\personas\*.yaml" Link="personas\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
25+
</ItemGroup>
2226
</Project>

src/CloudEngAgent.Application/Abstractions/IPersonaRepository.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,23 @@ public interface IPersonaRepository
77
Task<AgentPersona?> GetAsync(string id, CancellationToken cancellationToken);
88

99
IAsyncEnumerable<AgentPersona> ListAsync(CancellationToken cancellationToken);
10+
11+
/// <summary>
12+
/// Raised when the underlying persona source detects an add, update, or removal.
13+
/// Implementations that cannot detect changes (e.g., in-memory) never raise this event.
14+
/// </summary>
15+
event EventHandler<PersonaChangedEventArgs>? PersonaChanged;
16+
}
17+
18+
public enum PersonaChangeKind
19+
{
20+
Added,
21+
Updated,
22+
Removed,
23+
}
24+
25+
public sealed class PersonaChangedEventArgs(string personaId, PersonaChangeKind kind) : EventArgs
26+
{
27+
public string PersonaId { get; } = personaId;
28+
public PersonaChangeKind Kind { get; } = kind;
1029
}

src/CloudEngAgent.Infrastructure/Personas/InMemoryPersonaRepository.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ public InMemoryPersonaRepository()
2020
_personas = SeedPersonas().ToDictionary(p => p.Id, StringComparer.Ordinal);
2121
}
2222

23+
#pragma warning disable CS0067
24+
public event EventHandler<PersonaChangedEventArgs>? PersonaChanged;
25+
#pragma warning restore CS0067
26+
2327
public Task<AgentPersona?> GetAsync(string id, CancellationToken cancellationToken)
2428
{
2529
ArgumentException.ThrowIfNullOrWhiteSpace(id);

0 commit comments

Comments
 (0)