mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-23 11:55:12 +00:00
Moving the copilot-sdk cookbook content in here
This commit is contained in:
19
cookbook/copilot-sdk/dotnet/README.md
Normal file
19
cookbook/copilot-sdk/dotnet/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# GitHub Copilot SDK Cookbook — .NET (C#)
|
||||
|
||||
This folder hosts short, practical recipes for using the GitHub Copilot SDK with .NET. Each recipe is concise, copy‑pasteable, and points to fuller examples and tests.
|
||||
|
||||
## Recipes
|
||||
|
||||
- [Error Handling](error-handling.md): Handle errors gracefully including connection failures, timeouts, and cleanup.
|
||||
- [Multiple Sessions](multiple-sessions.md): Manage multiple independent conversations simultaneously.
|
||||
- [Managing Local Files](managing-local-files.md): Organize files by metadata using AI-powered grouping strategies.
|
||||
- [PR Visualization](pr-visualization.md): Generate interactive PR age charts using GitHub MCP Server.
|
||||
- [Persisting Sessions](persisting-sessions.md): Save and resume sessions across restarts.
|
||||
|
||||
## Contributing
|
||||
|
||||
Add a new recipe by creating a markdown file in this folder and linking it above. Follow repository guidance in [CONTRIBUTING.md](../../CONTRIBUTING.md).
|
||||
|
||||
## Status
|
||||
|
||||
This README is a scaffold; recipe files are placeholders until populated.
|
||||
156
cookbook/copilot-sdk/dotnet/error-handling.md
Normal file
156
cookbook/copilot-sdk/dotnet/error-handling.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Error Handling Patterns
|
||||
|
||||
Handle errors gracefully in your Copilot SDK applications.
|
||||
|
||||
> **Runnable example:** [recipe/error-handling.cs](recipe/error-handling.cs)
|
||||
>
|
||||
> ```bash
|
||||
> dotnet run recipe/error-handling.cs
|
||||
> ```
|
||||
|
||||
## Example scenario
|
||||
|
||||
You need to handle various error conditions like connection failures, timeouts, and invalid responses.
|
||||
|
||||
## Basic try-catch
|
||||
|
||||
```csharp
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
var client = new CopilotClient();
|
||||
|
||||
try
|
||||
{
|
||||
await client.StartAsync();
|
||||
var session = await client.CreateSessionAsync(new SessionConfig
|
||||
{
|
||||
Model = "gpt-5"
|
||||
});
|
||||
|
||||
var done = new TaskCompletionSource<string>();
|
||||
session.On(evt =>
|
||||
{
|
||||
if (evt is AssistantMessageEvent msg)
|
||||
{
|
||||
done.SetResult(msg.Data.Content);
|
||||
}
|
||||
});
|
||||
|
||||
await session.SendAsync(new MessageOptions { Prompt = "Hello!" });
|
||||
var response = await done.Task;
|
||||
Console.WriteLine(response);
|
||||
|
||||
await session.DisposeAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await client.StopAsync();
|
||||
}
|
||||
```
|
||||
|
||||
## Handling specific error types
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
await client.StartAsync();
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
Console.WriteLine("Copilot CLI not found. Please install it first.");
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.Message.Contains("connection"))
|
||||
{
|
||||
Console.WriteLine("Could not connect to Copilot CLI server.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Unexpected error: {ex.Message}");
|
||||
}
|
||||
```
|
||||
|
||||
## Timeout handling
|
||||
|
||||
```csharp
|
||||
var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" });
|
||||
|
||||
try
|
||||
{
|
||||
var done = new TaskCompletionSource<string>();
|
||||
session.On(evt =>
|
||||
{
|
||||
if (evt is AssistantMessageEvent msg)
|
||||
{
|
||||
done.SetResult(msg.Data.Content);
|
||||
}
|
||||
});
|
||||
|
||||
await session.SendAsync(new MessageOptions { Prompt = "Complex question..." });
|
||||
|
||||
// Wait with timeout (30 seconds)
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
var response = await done.Task.WaitAsync(cts.Token);
|
||||
|
||||
Console.WriteLine(response);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Console.WriteLine("Request timed out");
|
||||
}
|
||||
```
|
||||
|
||||
## Aborting a request
|
||||
|
||||
```csharp
|
||||
var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" });
|
||||
|
||||
// Start a request
|
||||
await session.SendAsync(new MessageOptions { Prompt = "Write a very long story..." });
|
||||
|
||||
// Abort it after some condition
|
||||
await Task.Delay(5000);
|
||||
await session.AbortAsync();
|
||||
Console.WriteLine("Request aborted");
|
||||
```
|
||||
|
||||
## Graceful shutdown
|
||||
|
||||
```csharp
|
||||
Console.CancelKeyPress += async (sender, e) =>
|
||||
{
|
||||
e.Cancel = true;
|
||||
Console.WriteLine("Shutting down...");
|
||||
|
||||
var errors = await client.StopAsync();
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
Console.WriteLine($"Cleanup errors: {string.Join(", ", errors)}");
|
||||
}
|
||||
|
||||
Environment.Exit(0);
|
||||
};
|
||||
```
|
||||
|
||||
## Using await using for automatic disposal
|
||||
|
||||
```csharp
|
||||
await using var client = new CopilotClient();
|
||||
await client.StartAsync();
|
||||
|
||||
var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" });
|
||||
|
||||
// ... do work ...
|
||||
|
||||
// client.StopAsync() is automatically called when exiting scope
|
||||
```
|
||||
|
||||
## Best practices
|
||||
|
||||
1. **Always clean up**: Use try-finally or `await using` to ensure `StopAsync()` is called
|
||||
2. **Handle connection errors**: The CLI might not be installed or running
|
||||
3. **Set appropriate timeouts**: Use `CancellationToken` for long-running requests
|
||||
4. **Log errors**: Capture error details for debugging
|
||||
138
cookbook/copilot-sdk/dotnet/managing-local-files.md
Normal file
138
cookbook/copilot-sdk/dotnet/managing-local-files.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Grouping Files by Metadata
|
||||
|
||||
Use Copilot to intelligently organize files in a folder based on their metadata.
|
||||
|
||||
> **Runnable example:** [recipe/managing-local-files.cs](recipe/managing-local-files.cs)
|
||||
>
|
||||
> ```bash
|
||||
> dotnet run recipe/managing-local-files.cs
|
||||
> ```
|
||||
|
||||
## Example scenario
|
||||
|
||||
You have a folder with many files and want to organize them into subfolders based on metadata like file type, creation date, size, or other attributes. Copilot can analyze the files and suggest or execute a grouping strategy.
|
||||
|
||||
## Example code
|
||||
|
||||
```csharp
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
// Create and start client
|
||||
await using var client = new CopilotClient();
|
||||
await client.StartAsync();
|
||||
|
||||
// Define tools for file operations
|
||||
var session = await client.CreateSessionAsync(new SessionConfig
|
||||
{
|
||||
Model = "gpt-5"
|
||||
});
|
||||
|
||||
// Wait for completion
|
||||
var done = new TaskCompletionSource();
|
||||
|
||||
session.On(evt =>
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case AssistantMessageEvent msg:
|
||||
Console.WriteLine($"\nCopilot: {msg.Data.Content}");
|
||||
break;
|
||||
case ToolExecutionStartEvent toolStart:
|
||||
Console.WriteLine($" → Running: {toolStart.Data.ToolName} ({toolStart.Data.ToolCallId})");
|
||||
break;
|
||||
case ToolExecutionCompleteEvent toolEnd:
|
||||
Console.WriteLine($" ✓ Completed: {toolEnd.Data.ToolCallId}");
|
||||
break;
|
||||
case SessionIdleEvent:
|
||||
done.SetResult();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Ask Copilot to organize files
|
||||
var targetFolder = @"C:\Users\Me\Downloads";
|
||||
|
||||
await session.SendAsync(new MessageOptions
|
||||
{
|
||||
Prompt = $"""
|
||||
Analyze the files in "{targetFolder}" and organize them into subfolders.
|
||||
|
||||
1. First, list all files and their metadata
|
||||
2. Preview grouping by file extension
|
||||
3. Create appropriate subfolders (e.g., "images", "documents", "videos")
|
||||
4. Move each file to its appropriate subfolder
|
||||
|
||||
Please confirm before moving any files.
|
||||
"""
|
||||
});
|
||||
|
||||
await done.Task;
|
||||
```
|
||||
|
||||
## Grouping strategies
|
||||
|
||||
### By file extension
|
||||
|
||||
```csharp
|
||||
// Groups files like:
|
||||
// images/ -> .jpg, .png, .gif
|
||||
// documents/ -> .pdf, .docx, .txt
|
||||
// videos/ -> .mp4, .avi, .mov
|
||||
```
|
||||
|
||||
### By creation date
|
||||
|
||||
```csharp
|
||||
// Groups files like:
|
||||
// 2024-01/ -> files created in January 2024
|
||||
// 2024-02/ -> files created in February 2024
|
||||
```
|
||||
|
||||
### By file size
|
||||
|
||||
```csharp
|
||||
// Groups files like:
|
||||
// tiny-under-1kb/
|
||||
// small-under-1mb/
|
||||
// medium-under-100mb/
|
||||
// large-over-100mb/
|
||||
```
|
||||
|
||||
## Dry-run mode
|
||||
|
||||
For safety, you can ask Copilot to only preview changes:
|
||||
|
||||
```csharp
|
||||
await session.SendAsync(new MessageOptions
|
||||
{
|
||||
Prompt = $"""
|
||||
Analyze files in "{targetFolder}" and show me how you would organize them
|
||||
by file type. DO NOT move any files - just show me the plan.
|
||||
"""
|
||||
});
|
||||
```
|
||||
|
||||
## Custom grouping with AI analysis
|
||||
|
||||
Let Copilot determine the best grouping based on file content:
|
||||
|
||||
```csharp
|
||||
await session.SendAsync(new MessageOptions
|
||||
{
|
||||
Prompt = $"""
|
||||
Look at the files in "{targetFolder}" and suggest a logical organization.
|
||||
Consider:
|
||||
- File names and what they might contain
|
||||
- File types and their typical uses
|
||||
- Date patterns that might indicate projects or events
|
||||
|
||||
Propose folder names that are descriptive and useful.
|
||||
"""
|
||||
});
|
||||
```
|
||||
|
||||
## Safety considerations
|
||||
|
||||
1. **Confirm before moving**: Ask Copilot to confirm before executing moves
|
||||
1. **Handle duplicates**: Consider what happens if a file with the same name exists
|
||||
1. **Preserve originals**: Consider copying instead of moving for important files
|
||||
79
cookbook/copilot-sdk/dotnet/multiple-sessions.md
Normal file
79
cookbook/copilot-sdk/dotnet/multiple-sessions.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Working with Multiple Sessions
|
||||
|
||||
Manage multiple independent conversations simultaneously.
|
||||
|
||||
> **Runnable example:** [recipe/multiple-sessions.cs](recipe/multiple-sessions.cs)
|
||||
>
|
||||
> ```bash
|
||||
> dotnet run recipe/multiple-sessions.cs
|
||||
> ```
|
||||
|
||||
## Example scenario
|
||||
|
||||
You need to run multiple conversations in parallel, each with its own context and history.
|
||||
|
||||
## C#
|
||||
|
||||
```csharp
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
await using var client = new CopilotClient();
|
||||
await client.StartAsync();
|
||||
|
||||
// Create multiple independent sessions
|
||||
var session1 = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" });
|
||||
var session2 = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" });
|
||||
var session3 = await client.CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-4.5" });
|
||||
|
||||
// Each session maintains its own conversation history
|
||||
await session1.SendAsync(new MessageOptions { Prompt = "You are helping with a Python project" });
|
||||
await session2.SendAsync(new MessageOptions { Prompt = "You are helping with a TypeScript project" });
|
||||
await session3.SendAsync(new MessageOptions { Prompt = "You are helping with a Go project" });
|
||||
|
||||
// Follow-up messages stay in their respective contexts
|
||||
await session1.SendAsync(new MessageOptions { Prompt = "How do I create a virtual environment?" });
|
||||
await session2.SendAsync(new MessageOptions { Prompt = "How do I set up tsconfig?" });
|
||||
await session3.SendAsync(new MessageOptions { Prompt = "How do I initialize a module?" });
|
||||
|
||||
// Clean up all sessions
|
||||
await session1.DisposeAsync();
|
||||
await session2.DisposeAsync();
|
||||
await session3.DisposeAsync();
|
||||
```
|
||||
|
||||
## Custom session IDs
|
||||
|
||||
Use custom IDs for easier tracking:
|
||||
|
||||
```csharp
|
||||
var session = await client.CreateSessionAsync(new SessionConfig
|
||||
{
|
||||
SessionId = "user-123-chat",
|
||||
Model = "gpt-5"
|
||||
});
|
||||
|
||||
Console.WriteLine(session.SessionId); // "user-123-chat"
|
||||
```
|
||||
|
||||
## Listing sessions
|
||||
|
||||
```csharp
|
||||
var sessions = await client.ListSessionsAsync();
|
||||
foreach (var sessionInfo in sessions)
|
||||
{
|
||||
Console.WriteLine($"Session: {sessionInfo.SessionId}");
|
||||
}
|
||||
```
|
||||
|
||||
## Deleting sessions
|
||||
|
||||
```csharp
|
||||
// Delete a specific session
|
||||
await client.DeleteSessionAsync("user-123-chat");
|
||||
```
|
||||
|
||||
## Use cases
|
||||
|
||||
- **Multi-user applications**: One session per user
|
||||
- **Multi-task workflows**: Separate sessions for different tasks
|
||||
- **A/B testing**: Compare responses from different models
|
||||
90
cookbook/copilot-sdk/dotnet/persisting-sessions.md
Normal file
90
cookbook/copilot-sdk/dotnet/persisting-sessions.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Session Persistence and Resumption
|
||||
|
||||
Save and restore conversation sessions across application restarts.
|
||||
|
||||
## Example scenario
|
||||
|
||||
You want users to be able to continue a conversation even after closing and reopening your application.
|
||||
|
||||
> **Runnable example:** [recipe/persisting-sessions.cs](recipe/persisting-sessions.cs)
|
||||
>
|
||||
> ```bash
|
||||
> cd recipe
|
||||
> dotnet run persisting-sessions.cs
|
||||
> ```
|
||||
|
||||
### Creating a session with a custom ID
|
||||
|
||||
```csharp
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
await using var client = new CopilotClient();
|
||||
await client.StartAsync();
|
||||
|
||||
// Create session with a memorable ID
|
||||
var session = await client.CreateSessionAsync(new SessionConfig
|
||||
{
|
||||
SessionId = "user-123-conversation",
|
||||
Model = "gpt-5"
|
||||
});
|
||||
|
||||
await session.SendAsync(new MessageOptions { Prompt = "Let's discuss TypeScript generics" });
|
||||
|
||||
// Session ID is preserved
|
||||
Console.WriteLine(session.SessionId); // "user-123-conversation"
|
||||
|
||||
// Destroy session but keep data on disk
|
||||
await session.DisposeAsync();
|
||||
await client.StopAsync();
|
||||
```
|
||||
|
||||
### Resuming a session
|
||||
|
||||
```csharp
|
||||
await using var client = new CopilotClient();
|
||||
await client.StartAsync();
|
||||
|
||||
// Resume the previous session
|
||||
var session = await client.ResumeSessionAsync("user-123-conversation");
|
||||
|
||||
// Previous context is restored
|
||||
await session.SendAsync(new MessageOptions { Prompt = "What were we discussing?" });
|
||||
|
||||
await session.DisposeAsync();
|
||||
await client.StopAsync();
|
||||
```
|
||||
|
||||
### Listing available sessions
|
||||
|
||||
```csharp
|
||||
var sessions = await client.ListSessionsAsync();
|
||||
foreach (var s in sessions)
|
||||
{
|
||||
Console.WriteLine($"Session: {s.SessionId}");
|
||||
}
|
||||
```
|
||||
|
||||
### Deleting a session permanently
|
||||
|
||||
```csharp
|
||||
// Remove session and all its data from disk
|
||||
await client.DeleteSessionAsync("user-123-conversation");
|
||||
```
|
||||
|
||||
### Getting session history
|
||||
|
||||
Retrieve all messages from a session:
|
||||
|
||||
```csharp
|
||||
var messages = await session.GetMessagesAsync();
|
||||
foreach (var msg in messages)
|
||||
{
|
||||
Console.WriteLine($"[{msg.Type}] {msg.Data.Content}");
|
||||
}
|
||||
```
|
||||
|
||||
## Best practices
|
||||
|
||||
1. **Use meaningful session IDs**: Include user ID or context in the session ID
|
||||
2. **Handle missing sessions**: Check if a session exists before resuming
|
||||
3. **Clean up old sessions**: Periodically delete sessions that are no longer needed
|
||||
257
cookbook/copilot-sdk/dotnet/pr-visualization.md
Normal file
257
cookbook/copilot-sdk/dotnet/pr-visualization.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# Generating PR Age Charts
|
||||
|
||||
Build an interactive CLI tool that visualizes pull request age distribution for a GitHub repository using Copilot's built-in capabilities.
|
||||
|
||||
> **Runnable example:** [recipe/pr-visualization.cs](recipe/pr-visualization.cs)
|
||||
>
|
||||
> ```bash
|
||||
> # Auto-detect from current git repo
|
||||
> dotnet run recipe/pr-visualization.cs
|
||||
>
|
||||
> # Specify a repo explicitly
|
||||
> dotnet run recipe/pr-visualization.cs -- --repo github/copilot-sdk
|
||||
> ```
|
||||
|
||||
## Example scenario
|
||||
|
||||
You want to understand how long PRs have been open in a repository. This tool detects the current Git repo or accepts a repo as input, then lets Copilot fetch PR data via the GitHub MCP Server and generate a chart image.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
dotnet add package GitHub.Copilot.SDK
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Auto-detect from current git repo
|
||||
dotnet run
|
||||
|
||||
# Specify a repo explicitly
|
||||
dotnet run -- --repo github/copilot-sdk
|
||||
```
|
||||
|
||||
## Full example: Program.cs
|
||||
|
||||
```csharp
|
||||
using System.Diagnostics;
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
// ============================================================================
|
||||
// Git & GitHub Detection
|
||||
// ============================================================================
|
||||
|
||||
bool IsGitRepo()
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "git",
|
||||
Arguments = "rev-parse --git-dir",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
})?.WaitForExit();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
string? GetGitHubRemote()
|
||||
{
|
||||
try
|
||||
{
|
||||
var proc = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "git",
|
||||
Arguments = "remote get-url origin",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
});
|
||||
|
||||
var remoteUrl = proc?.StandardOutput.ReadToEnd().Trim();
|
||||
proc?.WaitForExit();
|
||||
|
||||
if (string.IsNullOrEmpty(remoteUrl)) return null;
|
||||
|
||||
// Handle SSH: git@github.com:owner/repo.git
|
||||
var sshMatch = System.Text.RegularExpressions.Regex.Match(
|
||||
remoteUrl, @"git@github\.com:(.+/.+?)(?:\.git)?$");
|
||||
if (sshMatch.Success) return sshMatch.Groups[1].Value;
|
||||
|
||||
// Handle HTTPS: https://github.com/owner/repo.git
|
||||
var httpsMatch = System.Text.RegularExpressions.Regex.Match(
|
||||
remoteUrl, @"https://github\.com/(.+/.+?)(?:\.git)?$");
|
||||
if (httpsMatch.Success) return httpsMatch.Groups[1].Value;
|
||||
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
string? ParseRepoArg(string[] args)
|
||||
{
|
||||
var repoIndex = Array.IndexOf(args, "--repo");
|
||||
if (repoIndex != -1 && repoIndex + 1 < args.Length)
|
||||
{
|
||||
return args[repoIndex + 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
string PromptForRepo()
|
||||
{
|
||||
Console.Write("Enter GitHub repo (owner/repo): ");
|
||||
return Console.ReadLine()?.Trim() ?? "";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main Application
|
||||
// ============================================================================
|
||||
|
||||
Console.WriteLine("🔍 PR Age Chart Generator\n");
|
||||
|
||||
// Determine the repository
|
||||
var repo = ParseRepoArg(args);
|
||||
|
||||
if (!string.IsNullOrEmpty(repo))
|
||||
{
|
||||
Console.WriteLine($"📦 Using specified repo: {repo}");
|
||||
}
|
||||
else if (IsGitRepo())
|
||||
{
|
||||
var detected = GetGitHubRemote();
|
||||
if (detected != null)
|
||||
{
|
||||
repo = detected;
|
||||
Console.WriteLine($"📦 Detected GitHub repo: {repo}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("⚠️ Git repo found but no GitHub remote detected.");
|
||||
repo = PromptForRepo();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("📁 Not in a git repository.");
|
||||
repo = PromptForRepo();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(repo) || !repo.Contains('/'))
|
||||
{
|
||||
Console.WriteLine("❌ Invalid repo format. Expected: owner/repo");
|
||||
return;
|
||||
}
|
||||
|
||||
var parts = repo.Split('/');
|
||||
var owner = parts[0];
|
||||
var repoName = parts[1];
|
||||
|
||||
// Create Copilot client - no custom tools needed!
|
||||
await using var client = new CopilotClient(new CopilotClientOptions { LogLevel = "error" });
|
||||
await client.StartAsync();
|
||||
|
||||
var session = await client.CreateSessionAsync(new SessionConfig
|
||||
{
|
||||
Model = "gpt-5",
|
||||
SystemMessage = new SystemMessageConfig
|
||||
{
|
||||
Content = $"""
|
||||
<context>
|
||||
You are analyzing pull requests for the GitHub repository: {owner}/{repoName}
|
||||
The current working directory is: {Environment.CurrentDirectory}
|
||||
</context>
|
||||
|
||||
<instructions>
|
||||
- Use the GitHub MCP Server tools to fetch PR data
|
||||
- Use your file and code execution tools to generate charts
|
||||
- Save any generated images to the current working directory
|
||||
- Be concise in your responses
|
||||
</instructions>
|
||||
"""
|
||||
}
|
||||
});
|
||||
|
||||
// Set up event handling
|
||||
session.On(evt =>
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case AssistantMessageEvent msg:
|
||||
Console.WriteLine($"\n🤖 {msg.Data.Content}\n");
|
||||
break;
|
||||
case ToolExecutionStartEvent toolStart:
|
||||
Console.WriteLine($" ⚙️ {toolStart.Data.ToolName}");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Initial prompt - let Copilot figure out the details
|
||||
Console.WriteLine("\n📊 Starting analysis...\n");
|
||||
|
||||
await session.SendAsync(new MessageOptions
|
||||
{
|
||||
Prompt = $"""
|
||||
Fetch the open pull requests for {owner}/{repoName} from the last week.
|
||||
Calculate the age of each PR in days.
|
||||
Then generate a bar chart image showing the distribution of PR ages
|
||||
(group them into sensible buckets like <1 day, 1-3 days, etc.).
|
||||
Save the chart as "pr-age-chart.png" in the current directory.
|
||||
Finally, summarize the PR health - average age, oldest PR, and how many might be considered stale.
|
||||
"""
|
||||
});
|
||||
|
||||
// Interactive loop
|
||||
Console.WriteLine("\n💡 Ask follow-up questions or type \"exit\" to quit.\n");
|
||||
Console.WriteLine("Examples:");
|
||||
Console.WriteLine(" - \"Expand to the last month\"");
|
||||
Console.WriteLine(" - \"Show me the 5 oldest PRs\"");
|
||||
Console.WriteLine(" - \"Generate a pie chart instead\"");
|
||||
Console.WriteLine(" - \"Group by author instead of age\"");
|
||||
Console.WriteLine();
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.Write("You: ");
|
||||
var input = Console.ReadLine()?.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(input)) continue;
|
||||
if (input.ToLower() is "exit" or "quit")
|
||||
{
|
||||
Console.WriteLine("👋 Goodbye!");
|
||||
break;
|
||||
}
|
||||
|
||||
await session.SendAsync(new MessageOptions { Prompt = input });
|
||||
}
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
1. **Repository detection**: Checks `--repo` flag → git remote → prompts user
|
||||
2. **No custom tools**: Relies entirely on Copilot CLI's built-in capabilities:
|
||||
- **GitHub MCP Server** - Fetches PR data from GitHub
|
||||
- **File tools** - Saves generated chart images
|
||||
- **Code execution** - Generates charts using Python/matplotlib or other methods
|
||||
3. **Interactive session**: After initial analysis, user can ask for adjustments
|
||||
|
||||
## Why this approach?
|
||||
|
||||
| Aspect | Custom Tools | Built-in Copilot |
|
||||
| --------------- | ----------------- | --------------------------------- |
|
||||
| Code complexity | High | **Minimal** |
|
||||
| Maintenance | You maintain | **Copilot maintains** |
|
||||
| Flexibility | Fixed logic | **AI decides best approach** |
|
||||
| Chart types | What you coded | **Any type Copilot can generate** |
|
||||
| Data grouping | Hardcoded buckets | **Intelligent grouping** |
|
||||
55
cookbook/copilot-sdk/dotnet/recipe/README.md
Normal file
55
cookbook/copilot-sdk/dotnet/recipe/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Runnable Recipe Examples
|
||||
|
||||
This folder contains standalone, executable C# examples for each cookbook recipe. These are [file-based apps](https://learn.microsoft.com/dotnet/core/sdk/file-based-apps) that can be run directly with `dotnet run`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10.0 or later
|
||||
- GitHub Copilot SDK package (referenced automatically)
|
||||
|
||||
## Running Examples
|
||||
|
||||
Each `.cs` file is a complete, runnable program. Simply use:
|
||||
|
||||
```bash
|
||||
dotnet run <filename>.cs
|
||||
```
|
||||
|
||||
### Available Recipes
|
||||
|
||||
| Recipe | Command | Description |
|
||||
| -------------------- | ------------------------------------ | ------------------------------------------ |
|
||||
| Error Handling | `dotnet run error-handling.cs` | Demonstrates error handling patterns |
|
||||
| Multiple Sessions | `dotnet run multiple-sessions.cs` | Manages multiple independent conversations |
|
||||
| Managing Local Files | `dotnet run managing-local-files.cs` | Organizes files using AI grouping |
|
||||
| PR Visualization | `dotnet run pr-visualization.cs` | Generates PR age charts |
|
||||
| Persisting Sessions | `dotnet run persisting-sessions.cs` | Save and resume sessions across restarts |
|
||||
|
||||
### Examples with Arguments
|
||||
|
||||
**PR Visualization with specific repo:**
|
||||
|
||||
```bash
|
||||
dotnet run pr-visualization.cs -- --repo github/copilot-sdk
|
||||
```
|
||||
|
||||
**Managing Local Files (edit the file to change target folder):**
|
||||
|
||||
```bash
|
||||
# Edit the targetFolder variable in managing-local-files.cs first
|
||||
dotnet run managing-local-files.cs
|
||||
```
|
||||
|
||||
## File-Based Apps
|
||||
|
||||
These examples use .NET's file-based app feature, which allows single-file C# programs to:
|
||||
|
||||
- Run without a project file
|
||||
- Automatically reference common packages
|
||||
- Support top-level statements
|
||||
|
||||
## Learning Resources
|
||||
|
||||
- [.NET File-Based Apps Documentation](https://learn.microsoft.com/en-us/dotnet/core/sdk/file-based-apps)
|
||||
- [GitHub Copilot SDK Documentation](https://github.com/github/copilot-sdk/blob/main/dotnet/README.md)
|
||||
- [Parent Cookbook](../README.md)
|
||||
38
cookbook/copilot-sdk/dotnet/recipe/error-handling.cs
Normal file
38
cookbook/copilot-sdk/dotnet/recipe/error-handling.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
#:package GitHub.Copilot.SDK@*
|
||||
#:property PublishAot=false
|
||||
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
var client = new CopilotClient();
|
||||
|
||||
try
|
||||
{
|
||||
await client.StartAsync();
|
||||
var session = await client.CreateSessionAsync(new SessionConfig
|
||||
{
|
||||
Model = "gpt-5"
|
||||
});
|
||||
|
||||
var done = new TaskCompletionSource<string>();
|
||||
session.On(evt =>
|
||||
{
|
||||
if (evt is AssistantMessageEvent msg)
|
||||
{
|
||||
done.SetResult(msg.Data.Content);
|
||||
}
|
||||
});
|
||||
|
||||
await session.SendAsync(new MessageOptions { Prompt = "Hello!" });
|
||||
var response = await done.Task;
|
||||
Console.WriteLine(response);
|
||||
|
||||
await session.DisposeAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await client.StopAsync();
|
||||
}
|
||||
56
cookbook/copilot-sdk/dotnet/recipe/managing-local-files.cs
Normal file
56
cookbook/copilot-sdk/dotnet/recipe/managing-local-files.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
#:package GitHub.Copilot.SDK@*
|
||||
#:property PublishAot=false
|
||||
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
// Create and start client
|
||||
await using var client = new CopilotClient();
|
||||
await client.StartAsync();
|
||||
|
||||
// Define tools for file operations
|
||||
var session = await client.CreateSessionAsync(new SessionConfig
|
||||
{
|
||||
Model = "gpt-5"
|
||||
});
|
||||
|
||||
// Wait for completion
|
||||
var done = new TaskCompletionSource();
|
||||
|
||||
session.On(evt =>
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case AssistantMessageEvent msg:
|
||||
Console.WriteLine($"\nCopilot: {msg.Data.Content}");
|
||||
break;
|
||||
case ToolExecutionStartEvent toolStart:
|
||||
Console.WriteLine($" → Running: {toolStart.Data.ToolName} ({toolStart.Data.ToolCallId})");
|
||||
break;
|
||||
case ToolExecutionCompleteEvent toolEnd:
|
||||
Console.WriteLine($" ✓ Completed: {toolEnd.Data.ToolCallId}");
|
||||
break;
|
||||
case SessionIdleEvent:
|
||||
done.SetResult();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Ask Copilot to organize files
|
||||
// Change this to your target folder
|
||||
var targetFolder = @"C:\Users\Me\Downloads";
|
||||
|
||||
await session.SendAsync(new MessageOptions
|
||||
{
|
||||
Prompt = $"""
|
||||
Analyze the files in "{targetFolder}" and organize them into subfolders.
|
||||
|
||||
1. First, list all files and their metadata
|
||||
2. Preview grouping by file extension
|
||||
3. Create appropriate subfolders (e.g., "images", "documents", "videos")
|
||||
4. Move each file to its appropriate subfolder
|
||||
|
||||
Please confirm before moving any files.
|
||||
"""
|
||||
});
|
||||
|
||||
await done.Task;
|
||||
35
cookbook/copilot-sdk/dotnet/recipe/multiple-sessions.cs
Normal file
35
cookbook/copilot-sdk/dotnet/recipe/multiple-sessions.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
#:package GitHub.Copilot.SDK@*
|
||||
#:property PublishAot=false
|
||||
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
await using var client = new CopilotClient();
|
||||
await client.StartAsync();
|
||||
|
||||
// Create multiple independent sessions
|
||||
var session1 = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" });
|
||||
var session2 = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" });
|
||||
var session3 = await client.CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-4.5" });
|
||||
|
||||
Console.WriteLine("Created 3 independent sessions");
|
||||
|
||||
// Each session maintains its own conversation history
|
||||
await session1.SendAsync(new MessageOptions { Prompt = "You are helping with a Python project" });
|
||||
await session2.SendAsync(new MessageOptions { Prompt = "You are helping with a TypeScript project" });
|
||||
await session3.SendAsync(new MessageOptions { Prompt = "You are helping with a Go project" });
|
||||
|
||||
Console.WriteLine("Sent initial context to all sessions");
|
||||
|
||||
// Follow-up messages stay in their respective contexts
|
||||
await session1.SendAsync(new MessageOptions { Prompt = "How do I create a virtual environment?" });
|
||||
await session2.SendAsync(new MessageOptions { Prompt = "How do I set up tsconfig?" });
|
||||
await session3.SendAsync(new MessageOptions { Prompt = "How do I initialize a module?" });
|
||||
|
||||
Console.WriteLine("Sent follow-up questions to each session");
|
||||
|
||||
// Clean up all sessions
|
||||
await session1.DisposeAsync();
|
||||
await session2.DisposeAsync();
|
||||
await session3.DisposeAsync();
|
||||
|
||||
Console.WriteLine("All sessions destroyed successfully");
|
||||
38
cookbook/copilot-sdk/dotnet/recipe/persisting-sessions.cs
Normal file
38
cookbook/copilot-sdk/dotnet/recipe/persisting-sessions.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
#:package GitHub.Copilot.SDK@*
|
||||
#:property PublishAot=false
|
||||
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
await using var client = new CopilotClient();
|
||||
await client.StartAsync();
|
||||
|
||||
// Create session with a memorable ID
|
||||
var session = await client.CreateSessionAsync(new SessionConfig
|
||||
{
|
||||
SessionId = "user-123-conversation",
|
||||
Model = "gpt-5"
|
||||
});
|
||||
|
||||
await session.SendAsync(new MessageOptions { Prompt = "Let's discuss TypeScript generics" });
|
||||
Console.WriteLine($"Session created: {session.SessionId}");
|
||||
|
||||
// Destroy session but keep data on disk
|
||||
await session.DisposeAsync();
|
||||
Console.WriteLine("Session destroyed (state persisted)");
|
||||
|
||||
// Resume the previous session
|
||||
var resumed = await client.ResumeSessionAsync("user-123-conversation");
|
||||
Console.WriteLine($"Resumed: {resumed.SessionId}");
|
||||
|
||||
await resumed.SendAsync(new MessageOptions { Prompt = "What were we discussing?" });
|
||||
|
||||
// List sessions
|
||||
var sessions = await client.ListSessionsAsync();
|
||||
Console.WriteLine("Sessions: " + string.Join(", ", sessions.Select(s => s.SessionId)));
|
||||
|
||||
// Delete session permanently
|
||||
await client.DeleteSessionAsync("user-123-conversation");
|
||||
Console.WriteLine("Session deleted");
|
||||
|
||||
await resumed.DisposeAsync();
|
||||
await client.StopAsync();
|
||||
204
cookbook/copilot-sdk/dotnet/recipe/pr-visualization.cs
Normal file
204
cookbook/copilot-sdk/dotnet/recipe/pr-visualization.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
#:package GitHub.Copilot.SDK@*
|
||||
#:property PublishAot=false
|
||||
|
||||
using System.Diagnostics;
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
// ============================================================================
|
||||
// Git & GitHub Detection
|
||||
// ============================================================================
|
||||
|
||||
bool IsGitRepo()
|
||||
{
|
||||
try
|
||||
{
|
||||
var proc = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "git",
|
||||
Arguments = "rev-parse --git-dir",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
});
|
||||
proc?.WaitForExit();
|
||||
return proc?.ExitCode == 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
string? GetGitHubRemote()
|
||||
{
|
||||
try
|
||||
{
|
||||
var proc = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "git",
|
||||
Arguments = "remote get-url origin",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
});
|
||||
|
||||
var remoteUrl = proc?.StandardOutput.ReadToEnd().Trim();
|
||||
proc?.WaitForExit();
|
||||
|
||||
if (string.IsNullOrEmpty(remoteUrl)) return null;
|
||||
|
||||
// Handle SSH: git@github.com:owner/repo.git
|
||||
var sshMatch = System.Text.RegularExpressions.Regex.Match(
|
||||
remoteUrl, @"git@github\.com:(.+/.+?)(?:\.git)?$");
|
||||
if (sshMatch.Success) return sshMatch.Groups[1].Value;
|
||||
|
||||
// Handle HTTPS: https://github.com/owner/repo.git
|
||||
var httpsMatch = System.Text.RegularExpressions.Regex.Match(
|
||||
remoteUrl, @"https://github\.com/(.+/.+?)(?:\.git)?$");
|
||||
if (httpsMatch.Success) return httpsMatch.Groups[1].Value;
|
||||
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
string? ParseRepoArg(string[] args)
|
||||
{
|
||||
var repoIndex = Array.IndexOf(args, "--repo");
|
||||
if (repoIndex != -1 && repoIndex + 1 < args.Length)
|
||||
{
|
||||
return args[repoIndex + 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
string PromptForRepo()
|
||||
{
|
||||
Console.Write("Enter GitHub repo (owner/repo): ");
|
||||
return Console.ReadLine()?.Trim() ?? "";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main Application
|
||||
// ============================================================================
|
||||
|
||||
Console.WriteLine("🔍 PR Age Chart Generator\n");
|
||||
|
||||
// Determine the repository
|
||||
var repo = ParseRepoArg(args);
|
||||
|
||||
if (!string.IsNullOrEmpty(repo))
|
||||
{
|
||||
Console.WriteLine($"📦 Using specified repo: {repo}");
|
||||
}
|
||||
else if (IsGitRepo())
|
||||
{
|
||||
var detected = GetGitHubRemote();
|
||||
if (detected != null)
|
||||
{
|
||||
repo = detected;
|
||||
Console.WriteLine($"📦 Detected GitHub repo: {repo}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("⚠️ Git repo found but no GitHub remote detected.");
|
||||
repo = PromptForRepo();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("📁 Not in a git repository.");
|
||||
repo = PromptForRepo();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(repo) || !repo.Contains('/'))
|
||||
{
|
||||
Console.WriteLine("❌ Invalid repo format. Expected: owner/repo");
|
||||
return;
|
||||
}
|
||||
|
||||
var parts = repo.Split('/');
|
||||
var owner = parts[0];
|
||||
var repoName = parts[1];
|
||||
|
||||
// Create Copilot client - no custom tools needed!
|
||||
await using var client = new CopilotClient(new CopilotClientOptions { LogLevel = "error" });
|
||||
await client.StartAsync();
|
||||
|
||||
var session = await client.CreateSessionAsync(new SessionConfig
|
||||
{
|
||||
Model = "gpt-5",
|
||||
SystemMessage = new SystemMessageConfig
|
||||
{
|
||||
Content = $"""
|
||||
<context>
|
||||
You are analyzing pull requests for the GitHub repository: {owner}/{repoName}
|
||||
The current working directory is: {Environment.CurrentDirectory}
|
||||
</context>
|
||||
|
||||
<instructions>
|
||||
- Use the GitHub MCP Server tools to fetch PR data
|
||||
- Use your file and code execution tools to generate charts
|
||||
- Save any generated images to the current working directory
|
||||
- Be concise in your responses
|
||||
</instructions>
|
||||
"""
|
||||
}
|
||||
});
|
||||
|
||||
// Set up event handling
|
||||
session.On(evt =>
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case AssistantMessageEvent msg:
|
||||
Console.WriteLine($"\n🤖 {msg.Data.Content}\n");
|
||||
break;
|
||||
case ToolExecutionStartEvent toolStart:
|
||||
Console.WriteLine($" ⚙️ {toolStart.Data.ToolName}");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Initial prompt - let Copilot figure out the details
|
||||
Console.WriteLine("\n📊 Starting analysis...\n");
|
||||
|
||||
await session.SendAsync(new MessageOptions
|
||||
{
|
||||
Prompt = $"""
|
||||
Fetch the open pull requests for {owner}/{repoName} from the last week.
|
||||
Calculate the age of each PR in days.
|
||||
Then generate a bar chart image showing the distribution of PR ages
|
||||
(group them into sensible buckets like <1 day, 1-3 days, etc.).
|
||||
Save the chart as "pr-age-chart.png" in the current directory.
|
||||
Finally, summarize the PR health - average age, oldest PR, and how many might be considered stale.
|
||||
"""
|
||||
});
|
||||
|
||||
// Interactive loop
|
||||
Console.WriteLine("\n💡 Ask follow-up questions or type \"exit\" to quit.\n");
|
||||
Console.WriteLine("Examples:");
|
||||
Console.WriteLine(" - \"Expand to the last month\"");
|
||||
Console.WriteLine(" - \"Show me the 5 oldest PRs\"");
|
||||
Console.WriteLine(" - \"Generate a pie chart instead\"");
|
||||
Console.WriteLine(" - \"Group by author instead of age\"");
|
||||
Console.WriteLine();
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.Write("You: ");
|
||||
var input = Console.ReadLine()?.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(input)) continue;
|
||||
if (input.ToLower() is "exit" or "quit")
|
||||
{
|
||||
Console.WriteLine("👋 Goodbye!");
|
||||
break;
|
||||
}
|
||||
|
||||
await session.SendAsync(new MessageOptions { Prompt = input });
|
||||
}
|
||||
Reference in New Issue
Block a user