The Living Presentation - a fully interactive conference app that IS the demo. Every audience interaction (polls, questions, insights) runs on the .NET AI stack, locally, with no cloud required.
ConferenceAssistant is a live conference engagement platform built entirely on the modern .NET AI stack. It was designed as a conference talk where the application running on stage is the presentation - audience members scan a QR code, vote on AI-generated polls, ask questions, and watch AI agents analyze responses and generate insights in real time.
Six .NET technologies fire in sequence during the session:
| # | Technology | Role |
|---|---|---|
| 1 | Microsoft.Extensions.AI |
IChatClient / IEmbeddingGenerator abstractions |
| 2 | Microsoft.Extensions.DataIngestion |
RAG pipeline: chunk → enrich → embed → store |
| 3 | Microsoft.Extensions.VectorData |
Semantic search over session content |
| 4 | Microsoft.Agents.AI |
Specialized AI agents with tool calling |
| 5 | ModelContextProtocol |
MCP server exposing session tools to any AI client |
| 6 | .NET Aspire |
Orchestration of Ollama, Qdrant, PostgreSQL, and the web app |
┌─────────────────────────────────────────────────────┐
│ Blazor Server (Web) │
│ Presenter View │ Audience View │ Health Dashboard │
└────────────┬────────────────────────────┬────────────┘
│ │
┌───────▼────────┐ ┌────────▼────────┐
│ Agent Workflows│ │ MCP Server │
│ ┌───────────┐ │ │ /mcp endpoint │
│ │SurveyArch.│ │ │ 10 tools │
│ │ResponseAn.│ │ └────────┬────────┘
│ │KnowledgeCu│ │ │
│ │SessionSum.│ │ VS Code Copilot
│ └───────────┘ │ (or any MCP client)
└───────┬─────────┘
│
┌───────▼───────────────────────────────────────┐
│ Core Services │
│ PollService │ SessionService │ AgentTools │
└───────┬───────────────┬───────────────────────┘
│ │
┌───────▼──────┐ ┌──────▼──────────────┐
│ Ollama │ │ Qdrant │
│ llama3.2 / │ │ Vector store │
│ qwen2.5:7b │ │ (semantic search) │
│ nomic-embed │ └─────────────────────┘
└──────────────┘
- AI-generated polls - the Survey Architect agent reads the current topic, searches the knowledge base, and crafts targeted audience polls
- Real-time response analysis - the Response Analyst agent interprets poll results and stores data-driven insights
- Knowledge curation - session content is chunked, embedded, and made semantically searchable throughout the talk
- Audience Q&A - attendees submit questions; the Knowledge Curator agent answers them from session context
- Session summary - the Session Summary workflow generates a comprehensive post-session report
- MCP server - exposes 10 tools so VS Code Copilot (or any MCP client) can query live session data
- Health dashboard - real-time view of Ollama, Qdrant, and ingestion pipeline status
- QR code - attendees join the audience view by scanning a generated QR code
| Requirement | Version | Notes |
|---|---|---|
| .NET SDK | 10.0.108 | global.json pins the version |
| Docker Desktop | Latest | Runs Ollama, Qdrant, PostgreSQL via Aspire |
| .NET Aspire workload | 13.3+ | dotnet workload install aspire |
| Ollama models | - | Pulled automatically on first run |
No Azure or OpenAI account required. Everything runs locally via Docker.
Ollama runs the local LLM and embedding model. Install it from ollama.com, then pull the required models:
# Chat model (choose one - see model comparison table below)
ollama pull llama3.2
# Embedding model (required for semantic search)
ollama pull nomic-embed-textVerify Ollama is running:
curl http://localhost:11434/api/tagsWhen running via .NET Aspire, Ollama is started automatically inside Docker. Manual installation is only needed if you run the web app outside of Aspire.
Qdrant is the vector store used for semantic search. It is optional - the app falls back to an in-memory vector store if Qdrant is unavailable.
To run Qdrant manually via Docker:
docker run -p 6333:6333 -p 6334:6334 qdrant/qdrantTo disable Qdrant and use the in-memory store instead, set in appsettings.json:
"VectorStore": {
"Provider": "InMemory"
}When running via .NET Aspire, Qdrant is started automatically with a persistent data volume. No manual setup is required.
git clone https://github.com/your-org/ConferenceAssistant.git
cd ConferenceAssistantdotnet workload install aspiredotnet run --project src/ConferenceAssistant.AppHostAspire will start all containers automatically. On first run, Ollama will pull the required models (this may take a few minutes depending on your connection).
| URL | Purpose |
|---|---|
| Aspire dashboard | https://localhost:15888 |
| Presenter view | https://localhost:PORT/presenter |
| Audience view | https://localhost:PORT/audience |
| Health dashboard | https://localhost:PORT/health |
The exact port is shown in the Aspire dashboard under the web resource.
All settings are in src/ConferenceAssistant.Web/appsettings.json:
{
"Ollama": {
"Endpoint": "http://localhost:11434",
"ChatModel": "llama3.2",
"EmbeddingModel": "nomic-embed-text"
},
"VectorStore": {
"Provider": "Qdrant",
"QdrantEndpoint": "http://localhost:6334",
"QdrantHttpEndpoint": "http://localhost:6333"
},
"Session": {
"Code": "AICONF",
"OutlinePath": "data/session-outline.md",
"TopicsPath": "data/sessions.json",
"SlidesPath": "data/slides.md"
}
}Change ChatModel to any model available in Ollama. Recommended options for tool calling:
| Model | Quality | Speed | Tool calling |
|---|---|---|---|
llama3.2 (default) |
Good | Fast | Limited multi-step |
qwen2.5:7b |
Better | Moderate | Reliable multi-step |
phi4 |
Best | Slow | Reliable |
ollama pull qwen2.5:7bThen update appsettings.json:
"ChatModel": "qwen2.5:7b"Replace the files in data/ to run a different session:
| File | Purpose |
|---|---|
sessions.json |
Topic list with titles, descriptions, and status |
slides.md |
Slide content ingested into the knowledge base |
session-outline.md |
Speaker notes and poll trigger hints per segment |
src/
├── ConferenceAssistant.AppHost/ # .NET Aspire orchestration
├── ConferenceAssistant.Web/ # Blazor Server app (presenter, audience, health views)
├── ConferenceAssistant.Core/ # Domain models and in-memory services
├── ConferenceAssistant.Agents/ # AI agent workflows (Survey, Analysis, Curation, Summary)
├── ConferenceAssistant.Ingestion/ # RAG pipeline and semantic search
├── ConferenceAssistant.Mcp/ # MCP server tools
├── ConferenceAssistant.CopilotDemo/ # VS Code Copilot integration demo
└── ConferenceAssistant.ServiceDefaults/ # Shared Aspire service defaults
tests/
└── ConferenceAssistant.Evaluation/ # Agent quality / evaluation tests
data/
├── sessions.json # Session topics
├── slides.md # Slide content
└── session-outline.md # Speaker outline with poll triggers
docs/
├── AI-LEARNING-PATH.md # Microsoft.Extensions.AI learning resources
└── DOTNET-AGENT-LEARNING-PATH.md # Agent Framework learning resources
The app has four specialized agents, each with a focused persona and a minimal tool set:
| Agent | Role | Tools |
|---|---|---|
| Survey Architect | Generates audience polls from session context | GetCurrentTopic, SearchKnowledge, GetAudienceQuestions, GetAllPollResults, GetAllInsights, CreatePoll |
| Response Analyst | Interprets poll results and stores insights | StoreInsight (context pre-fetched in C#) |
| Knowledge Curator | Answers audience questions from session content | SearchKnowledge, SaveInsight |
| Session Summarizer | Generates end-of-session summary | GetAllInsights, GetAllPollResults, SearchKnowledge |
Architecture note: For small local models (≤4B parameters), all context is pre-fetched in C# before invoking the agent. The agent then performs a single, reliable tool call. This avoids the multi-step tool chaining that small models consistently fail at.
ConferenceAssistant exposes a Model Context Protocol server at /mcp. Connect any MCP-compatible client (VS Code Copilot, Claude Desktop, etc.) to query live session data.
Add to your .vscode/mcp.json:
{
"servers": {
"conference-pulse": {
"type": "http",
"url": "http://localhost:PORT/mcp"
}
}
}| Tool | Description |
|---|---|
get_session_status |
Current topic and all topic statuses |
get_outline |
Full session outline with slides |
get_active_poll |
Currently open poll |
get_all_insights |
All AI-generated insights |
get_insights_markdown |
Insights formatted as markdown |
get_audience_questions |
Questions submitted by the audience |
search_knowledge |
Semantic search over the session knowledge base |
get_knowledge_stats |
Knowledge base statistics |
generate_session_summary |
Trigger the Session Summary workflow |
The health check uses the HTTP REST endpoint (port 6333), not the gRPC endpoint (port 6334). Verify:
- Qdrant is running:
curl http://localhost:6333/healthz appsettings.jsonhas"QdrantHttpEndpoint": "http://localhost:6333"(not6334)
Error: model 'llama3.2' not found
Run ollama pull llama3.2 (or whichever model is set in ChatModel). Check available models with ollama list.
This happens when the agent calls multiple tools sequentially with a large model. Options:
- Use
llama3.2(3B, fastest) - works well with pre-fetched context workflows - The
ResponseAnalysisWorkflowand similar workflows pre-fetch all context in C# before invoking the agent, keeping inference to a single pass - For
PollGenerationWorkflow, switch toqwen2.5:7bfor reliable multi-step tool calling
This is a known behaviour of small local models (≤4B parameters). They treat the first meaningful tool response as "task complete" and stop. The solution is already applied in ResponseAnalysisWorkflow: pre-fetch all data in C# and give the agent a single tool to call. Apply the same pattern to any workflow showing this behaviour.
This occurs when StateHasChanged() is called from a background thread (e.g., a System.Threading.Timer callback). Fix:
// Wrong
_timer = new Timer(_ => StateHasChanged(), ...);
// Correct
_timer = new Timer(async _ => await InvokeAsync(StateHasChanged), ...);The ingestion pipeline runs on startup and embeds slides.md into Qdrant. If the knowledge base is empty:
- Check the Aspire dashboard logs for
OutlineIngestionPipelineorContentIngestionPipelineerrors - Ensure
VectorStore:ForceReingestisfalseon subsequent runs (set totrueto force a full re-ingest) - Verify Qdrant is healthy before the app starts
The domain model in ConferenceAssistant.Core follows DDD principles to keep business logic inside the entities themselves rather than scattered across services.
| Type | Class | Role |
|---|---|---|
| Aggregate Root | Poll |
Owns its lifecycle: Create() → Launch() → Close() / Reopen() |
| Aggregate Root | SessionTopic |
Controls topic status via Activate() / Complete() |
| Aggregate Root | Conference |
Configuration root, loaded from file |
| Entity | AudienceQuestion |
Submit() → Approve() / Reject() / Upvote() / SetAnswer() |
| Entity | Insight |
Immutable after Create() - no setters exposed |
| Entity | Slide |
Data entity parsed from markdown; no domain invariants |
| Value Object | PollResponse |
Immutable vote record; Cast() factory, never mutated |
| Value Object | PollPrompt |
sealed record embedded in topic configuration |
All entities inherit from Entity<TId> (identity + domain events list). Aggregate roots inherit AggregateRoot<TId>, marking them as the sole transaction boundary for their cluster.
Properties that represent domain state use private set so they can only change through named behavior methods:
// Before (anemic) - anyone can corrupt state:
poll.Status = PollStatus.Active;
poll.ClosedAt = null;
// After (DDD) - invariants are enforced:
poll.Launch(); // throws if not in Draft; raises PollLaunched event
poll.Close(); // throws if not Active; raises PollClosed event
poll.Reopen(); // throws if not Closed; raises PollReopened eventEvery meaningful state change raises a domain event that is stored on the entity until the service dispatches it:
PollCreated → PollLaunched → VoteCast (×N) → PollClosed → PollReopened
TopicActivated → TopicCompleted
QuestionSubmitted → QuestionApproved / QuestionRejected → QuestionAnswered / QuestionUpvoted
InsightStored
Events are plain record types implementing IDomainEvent:
public record PollClosed(string PollId, DateTimeOffset ClosedAt) : IDomainEvent
{
public DateTimeOffset OccurredAt { get; } = DateTimeOffset.UtcNow;
}DomainEventDispatcher resolves all registered IDomainEventHandler<TEvent> implementations via DI and invokes them fire-and-forget after any aggregate mutation, so service methods never block on side effects:
// In PollService:
poll.Close();
dispatcher.DispatchAndClear(poll); // fires PollClosed in backgroundThree handlers are registered at startup:
| Handler | Project | Reacts To | What It Does |
|---|---|---|---|
PollClosedHandler |
Agents |
PollClosed |
Automatically runs ResponseAnalysisWorkflow - no manual "Analyze" click needed |
TopicActivatedHandler |
Core |
TopicActivated |
Logs topic activation for analytics/audit |
InsightStoredHandler |
Core |
InsightStored |
Logs insight ID, type, and poll for tracing |
Adding a new handler takes two steps:
// 1. Implement the interface
public class MyHandler(SomeService svc) : IDomainEventHandler<PollLaunched>
{
public Task HandleAsync(PollLaunched @event, CancellationToken ct = default)
{
// react to the event
return Task.CompletedTask;
}
}
// 2. Register in Program.cs (multiple handlers per event are supported)
builder.Services.AddSingleton<IDomainEventHandler<PollLaunched>, MyHandler>();src/ConferenceAssistant.Core/
├── Models/
│ ├── Entity.cs ← base entity (Id + domain events)
│ ├── AggregateRoot.cs ← aggregate root marker
│ ├── Poll.cs ← aggregate: Launch/Close/Reopen
│ ├── AudienceQuestion.cs ← entity: Approve/Reject/Upvote/SetAnswer
│ ├── Insight.cs ← entity: immutable, Create() factory
│ ├── PollResponse.cs ← value object: Cast() factory
│ ├── SessionTopic.cs ← aggregate: Activate/Complete
│ ├── Conference.cs ← aggregate: configuration root
│ └── Slide.cs ← entity: parsed from markdown
└── Domain/
├── IDomainEvent.cs
├── IDomainEventHandler.cs
├── DomainEventDispatcher.cs
├── Events/
│ ├── PollEvents.cs ← PollCreated, PollLaunched, PollClosed, PollReopened, VoteCast
│ ├── QuestionEvents.cs ← QuestionSubmitted, Approved, Rejected, Upvoted, Answered
│ ├── TopicEvents.cs ← TopicActivated, TopicCompleted
│ └── InsightEvents.cs ← InsightStored
└── Handlers/
├── TopicActivatedHandler.cs
└── InsightStoredHandler.cs
src/ConferenceAssistant.Agents/
└── Handlers/
└── PollClosedHandler.cs ← auto-triggers ResponseAnalysisWorkflow
Small local models (llama3.2 3B, phi3-mini 3.8B) reliably exit after the first tool call that returns meaningful data. Prompting alone cannot override this - it is a model capacity limit, not a configuration problem.
Decision: All context gathering (poll results, knowledge search, duplicate checks) is done in C# before the agent is invoked. The agent receives a fully-populated prompt and only needs to call one tool (e.g., StoreInsight, CreatePoll). This makes workflows reliable on any model size.
All session state (polls, votes, questions, insights) is held in SessionService as in-memory collections. This makes the app self-contained for conference demos - no database migrations, no seed data, no connection strings to manage.
PostgreSQL is provisioned by Aspire but reserved for future persistence. The ForceReingest flag controls whether the vector store is repopulated on startup.
All LLM interaction goes through Microsoft.Extensions.AI's IChatClient. Switching from Ollama to Azure OpenAI, or from llama3.2 to qwen2.5:7b, requires changing one line in Program.cs (or one config value). No agent, workflow, or tool code changes.
All agent personas and numbered instructions live in AgentInstructions.cs. This makes it easy to tune prompts, compare versions in git diffs, and share instruction text between the agent and evaluation tests - without scattering strings across multiple files.
The app exposes a /mcp endpoint from day one, not as an afterthought. This means VS Code Copilot (or Claude Desktop, or any MCP client) can query live session data during the demo - reinforcing the talk's message that MCP is the standard protocol for AI tool integration.
Qdrant exposes two ports: gRPC (6334) for the vector store client, and HTTP REST (6333) for health checks and the dashboard. The app uses separate config keys (QdrantEndpoint for gRPC, QdrantHttpEndpoint for HTTP) to avoid probing the wrong port with HTTP health checks.
dotnet test tests/ConferenceAssistant.EvaluationThe evaluation tests assess agent output quality - poll relevance, insight accuracy, and knowledge curator responses - using the running Ollama instance.
Contributions are welcome. Please open an issue before submitting a pull request for significant changes.
- Fork the repo
- Create a feature branch (
git checkout -b feature/your-feature) - Commit your changes
- Open a pull request
Curated learning paths for the technologies used in this project:
- docs/AI-LEARNING-PATH.md -
Microsoft.Extensions.AIandMicrosoft.Agents.AI - docs/DOTNET-AGENT-LEARNING-PATH.md - Agent Framework deep dive
MIT - see LICENSE.