Skip to content

Commit 689963e

Browse files
committed
feat(cli): Introduce new enums for Format, Provider, and Tool; update DoCommandHandler for improved type safety
1 parent 6f34941 commit 689963e

File tree

13 files changed

+114
-126
lines changed

13 files changed

+114
-126
lines changed

.github/workflows/auto-labeling.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ jobs:
1414
issue-number: ${{ github.event.issue.number }}
1515
model: free-fast
1616
provider: openrouter
17+
instructions: 'if an issue looks simple and does not affect database, add one of aider labels to try to solve it automatically'
1718
secrets:
1819
personal-access-token: ${{ secrets.PERSONAL_TOKEN }}
1920
openrouter-api-key: ${{ secrets.OPENROUTER_API_KEY_FOR_PRS }}

src/Cli/src/Commands/DoCommandHandler.cs

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,38 @@
66
using System.Text;
77
using System.Text.Json;
88
using System.Text.Json.Schema;
9+
using LangChain.Cli.Models;
910
using Microsoft.Extensions.AI;
1011
using ModelContextProtocol;
1112
using ModelContextProtocol.Client;
1213
using ModelContextProtocol.Protocol.Transport;
1314
using ModelContextProtocol.Protocol.Types;
15+
using Tool = LangChain.Cli.Models.Tool;
1416

1517
namespace LangChain.Cli.Commands;
1618

1719
internal sealed class DoCommandHandler : ICommandHandler
1820
{
1921
public Option<string> InputOption { get; } = CommonOptions.Input;
20-
public Option<string> InputFileOption { get; } = CommonOptions.InputFile;
21-
public Option<string> OutputFileOption { get; } = CommonOptions.OutputFile;
22+
public Option<FileInfo?> InputFileOption { get; } = CommonOptions.InputFile;
23+
public Option<FileInfo?> OutputFileOption { get; } = CommonOptions.OutputFile;
2224
public Option<bool> DebugOption { get; } = CommonOptions.Debug;
2325
public Option<string> ModelOption { get; } = CommonOptions.Model;
24-
public Option<string> ProviderOption { get; } = CommonOptions.Provider;
25-
public Option<string[]> ToolsOption { get; } = new(
26+
public Option<Provider> ProviderOption { get; } = CommonOptions.Provider;
27+
public Option<Tool[]> ToolsOption { get; } = new(
2628
aliases: ["--tools", "-t"],
27-
parseArgument: result => result.Tokens.SelectMany(t => t.Value.Split(',')).ToArray(),
28-
description: $"Tools you want to use. Example: --tools={string.Join(",", Formats.All)}");
29-
public Option<string[]> DirectoriesOption { get; } = new(
29+
description: $"Tools you want to use - {string.Join(", ", Enum.GetNames<Tool>())}.")
30+
{
31+
AllowMultipleArgumentsPerToken = true,
32+
};
33+
public Option<DirectoryInfo[]> DirectoriesOption { get; } = new(
3034
aliases: ["--directories", "-d"],
31-
getDefaultValue: () => ["."],
35+
getDefaultValue: () => [new DirectoryInfo(".")],
3236
description: "Directories you want to use for filesystem.");
33-
public Option<string> FormatOption { get; } = new(
37+
public Option<Format> FormatOption { get; } = new(
3438
aliases: ["--format", "-f"],
35-
getDefaultValue: () => Formats.Text,
36-
description: $"Format of answer. Can be {string.Join(" or ", Formats.All)}.");
39+
getDefaultValue: () => Format.Text,
40+
description: "Format of answer.");
3741

3842
public int Invoke(InvocationContext context)
3943
{
@@ -43,7 +47,7 @@ public int Invoke(InvocationContext context)
4347
public async Task<int> InvokeAsync(InvocationContext context)
4448
{
4549
var input = context.ParseResult.GetValueForOption(InputOption) ?? string.Empty;
46-
var inputPath = context.ParseResult.GetValueForOption(InputFileOption) ?? string.Empty;
50+
var inputPath = context.ParseResult.GetValueForOption(InputFileOption);
4751
var outputPath = context.ParseResult.GetValueForOption(OutputFileOption);
4852
var debug = context.ParseResult.GetValueForOption(DebugOption);
4953
var model = context.ParseResult.GetValueForOption(ModelOption);
@@ -60,76 +64,76 @@ public async Task<int> InvokeAsync(InvocationContext context)
6064
return await McpClientFactory.CreateAsync(
6165
tool switch
6266
{
63-
Tools.Filesystem => new McpServerConfig
67+
Tool.Filesystem => new McpServerConfig
6468
{
65-
Id = Tools.Filesystem,
66-
Name = Tools.Filesystem,
69+
Id = Tool.Filesystem.ToString(),
70+
Name = Tool.Filesystem.ToString(),
6771
TransportType = TransportTypes.StdIo,
6872
TransportOptions = new Dictionary<string, string>
6973
{
7074
["command"] = "npx",
71-
["arguments"] = $"-y @modelcontextprotocol/server-filesystem {string.Join(' ', directories)}",
75+
["arguments"] = $"-y @modelcontextprotocol/server-filesystem {string.Join(' ', directories.Select(x => x.FullName))}",
7276
},
7377
},
74-
Tools.Fetch => new McpServerConfig
78+
Tool.Fetch => new McpServerConfig
7579
{
76-
Id = Tools.Fetch,
77-
Name = Tools.Fetch,
80+
Id = Tool.Fetch.ToString(),
81+
Name = Tool.Fetch.ToString(),
7882
TransportType = TransportTypes.StdIo,
7983
TransportOptions = new Dictionary<string, string>
8084
{
8185
["command"] = "docker",
8286
["arguments"] = "run -i --rm mcp/fetch",
8387
},
8488
},
85-
Tools.GitHub => new McpServerConfig
89+
Tool.GitHub => new McpServerConfig
8690
{
87-
Id = Tools.GitHub,
88-
Name = Tools.GitHub,
91+
Id = Tool.GitHub.ToString(),
92+
Name = Tool.GitHub.ToString(),
8993
TransportType = TransportTypes.StdIo,
9094
TransportOptions = new Dictionary<string, string>
9195
{
9296
["command"] = "docker",
9397
["arguments"] = $"run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN={Environment.GetEnvironmentVariable("GITHUB_TOKEN")} ghcr.io/github/github-mcp-server",
9498
},
9599
},
96-
Tools.Git => new McpServerConfig
100+
Tool.Git => new McpServerConfig
97101
{
98-
Id = Tools.Git,
99-
Name = Tools.Git,
102+
Id = Tool.Git.ToString(),
103+
Name = Tool.Git.ToString(),
100104
TransportType = TransportTypes.StdIo,
101105
TransportOptions = new Dictionary<string, string>
102106
{
103107
["command"] = "docker",
104108
["arguments"] = $"run -i --rm {string.Join(' ', directories.Select(x => $"--mount type=bind,src={x},dst={x} "))} mcp/git",
105109
},
106110
},
107-
Tools.Puppeteer => new McpServerConfig
111+
Tool.Puppeteer => new McpServerConfig
108112
{
109-
Id = Tools.Puppeteer,
110-
Name = Tools.Puppeteer,
113+
Id = Tool.Puppeteer.ToString(),
114+
Name = Tool.Puppeteer.ToString(),
111115
TransportType = TransportTypes.StdIo,
112116
TransportOptions = new Dictionary<string, string>
113117
{
114118
["command"] = "docker",
115119
["arguments"] = "run -i --rm --init -e DOCKER_CONTAINER=true mcp/puppeteer",
116120
},
117121
},
118-
Tools.SequentialThinking => new McpServerConfig
122+
Tool.SequentialThinking => new McpServerConfig
119123
{
120-
Id = Tools.SequentialThinking,
121-
Name = Tools.SequentialThinking,
124+
Id = Tool.SequentialThinking.ToString(),
125+
Name = Tool.SequentialThinking.ToString(),
122126
TransportType = TransportTypes.StdIo,
123127
TransportOptions = new Dictionary<string, string>
124128
{
125129
["command"] = "docker",
126130
["arguments"] = "run -i --rm mcp/sequentialthinking",
127131
},
128132
},
129-
Tools.Slack => new McpServerConfig
133+
Tool.Slack => new McpServerConfig
130134
{
131-
Id = Tools.Slack,
132-
Name = Tools.Slack,
135+
Id = Tool.Slack.ToString(),
136+
Name = Tool.Slack.ToString(),
133137
TransportType = TransportTypes.StdIo,
134138
TransportOptions = new Dictionary<string, string>
135139
{
@@ -167,7 +171,7 @@ public async Task<int> InvokeAsync(InvocationContext context)
167171
{
168172
Tools = [
169173
.. aiTools.SelectMany(x => x).ToArray(),
170-
.. tools.Contains("filesystem")
174+
.. tools.Contains(Tool.Filesystem)
171175
? new [] { AIFunctionFactory.Create(
172176
FindFilePathsByContent,
173177
name: "FindFilePathsByContent",
@@ -176,33 +180,33 @@ .. tools.Contains("filesystem")
176180
],
177181
ResponseFormat = format switch
178182
{
179-
Formats.Text => ChatResponseFormat.Text,
180-
Formats.Lines => ChatResponseFormatForType<StringArraySchema>(),
181-
Formats.ConventionalCommit => ChatResponseFormatForType<ConventionalCommitSchema>(
183+
Format.Text => ChatResponseFormat.Text,
184+
Format.Lines => ChatResponseFormatForType<StringArraySchema>(),
185+
Format.ConventionalCommit => ChatResponseFormatForType<ConventionalCommitSchema>(
182186
schemaName: "ConventionalCommitSchema",
183187
schemaDescription: "Conventional commit schema. Use this schema to generate conventional commits."),
184-
Formats.Markdown => ChatResponseFormatForType<MarkdownSchema>(
188+
Format.Markdown => ChatResponseFormatForType<MarkdownSchema>(
185189
schemaName: "MarkdownSchema",
186190
schemaDescription: "Markdown schema. Use this schema to generate markdown."),
187-
Formats.Json => ChatResponseFormat.Json,
191+
Format.Json => ChatResponseFormat.Json,
188192
_ => throw new ArgumentException($"Unknown format: {format}"),
189193
},
190194
}).ConfigureAwait(false);
191195

192196
var output = response.Text;
193-
if (format == Formats.Lines)
197+
if (format == Format.Lines)
194198
{
195199
var value = JsonSerializer.Deserialize<StringArraySchema>(response.Text);
196200

197201
output = string.Join(Environment.NewLine, value?.Value ?? []);
198202
}
199-
else if (format == Formats.Markdown)
203+
else if (format == Format.Markdown)
200204
{
201205
var value = JsonSerializer.Deserialize<MarkdownSchema>(response.Text);
202206

203207
output = value?.Markdown ?? string.Empty;
204208
}
205-
else if (format == Formats.ConventionalCommit)
209+
else if (format == Format.ConventionalCommit)
206210
{
207211
var value = JsonSerializer.Deserialize<ConventionalCommitSchema>(response.Text);
208212

src/Cli/src/CommonOptions.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.CommandLine;
2+
using LangChain.Cli.Models;
23

34
namespace LangChain.Cli;
45

@@ -9,14 +10,14 @@ internal static class CommonOptions
910
getDefaultValue: () => string.Empty,
1011
description: "Input text");
1112

12-
public static Option<string> InputFile => new(
13+
public static Option<FileInfo?> InputFile => new(
1314
aliases: ["--input-file"],
14-
getDefaultValue: () => string.Empty,
15+
getDefaultValue: () => null,
1516
description: "Input file path");
1617

17-
public static Option<string> OutputFile => new(
18+
public static Option<FileInfo?> OutputFile => new(
1819
aliases: ["--output-file"],
19-
getDefaultValue: () => string.Empty,
20+
getDefaultValue: () => null,
2021
description: "Output file path");
2122

2223
public static Option<bool> Debug => new(
@@ -27,10 +28,10 @@ internal static class CommonOptions
2728
public static Option<string> Model => new(
2829
aliases: ["--model"],
2930
getDefaultValue: () => "o3-mini",
30-
description: "Model to use for commands. Default is o3-mini.");
31+
description: "Model to use for commands.");
3132

32-
public static Option<string> Provider => new(
33+
public static Option<Provider> Provider => new(
3334
aliases: ["--provider"],
34-
getDefaultValue: () => Providers.OpenAi,
35-
description: $"Provider to use for commands. Default is {Providers.OpenAi}.");
35+
getDefaultValue: () => default,
36+
description: $"Provider to use for commands.");
3637
}

src/Cli/src/Formats.cs

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/Cli/src/Helpers.cs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.ClientModel;
22
using System.CommandLine;
33
using System.CommandLine.IO;
4+
using LangChain.Cli.Models;
45
using Microsoft.Extensions.AI;
56
using Microsoft.Extensions.Logging;
67
using OpenAI;
@@ -9,32 +10,32 @@ namespace LangChain.Cli;
910

1011
internal static class Helpers
1112
{
12-
public static async Task<string> ReadInputAsync(string input, string inputPath, CancellationToken cancellationToken = default)
13+
public static async Task<string> ReadInputAsync(string input, FileInfo? inputPath, CancellationToken cancellationToken = default)
1314
{
14-
if (string.IsNullOrWhiteSpace(input) && string.IsNullOrWhiteSpace(inputPath))
15+
if (string.IsNullOrWhiteSpace(input) && inputPath is null)
1516
{
1617
throw new ArgumentException("Either input or input file must be provided.");
1718
}
1819

1920
var inputText = input;
20-
if (!string.IsNullOrWhiteSpace(inputPath))
21+
if (inputPath is not null)
2122
{
2223
if (!string.IsNullOrWhiteSpace(inputText))
2324
{
2425
inputText += Environment.NewLine;
2526
}
2627

27-
inputText += await File.ReadAllTextAsync(inputPath, cancellationToken).ConfigureAwait(false);
28+
inputText += await File.ReadAllTextAsync(inputPath.FullName, cancellationToken).ConfigureAwait(false);
2829
}
2930

3031
return inputText;
3132
}
3233

33-
public static async Task WriteOutputAsync(string outputText, string? outputPath, IConsole? console = null, CancellationToken cancellationToken = default)
34+
public static async Task WriteOutputAsync(string outputText, FileInfo? outputPath, IConsole? console = null, CancellationToken cancellationToken = default)
3435
{
35-
if (!string.IsNullOrWhiteSpace(outputPath))
36+
if (outputPath is not null)
3637
{
37-
await File.WriteAllTextAsync(outputPath, outputText, cancellationToken).ConfigureAwait(false);
38+
await File.WriteAllTextAsync(outputPath.FullName, outputText, cancellationToken).ConfigureAwait(false);
3839
}
3940
else
4041
{
@@ -51,7 +52,7 @@ public static async Task WriteOutputAsync(string outputText, string? outputPath,
5152

5253
public static IChatClient GetChatModel(
5354
string? model = null,
54-
string? provider = null,
55+
Provider? provider = null,
5556
bool debug = false)
5657
{
5758
if (debug)
@@ -63,7 +64,7 @@ public static IChatClient GetChatModel(
6364
IChatClient chatClient;
6465
Uri? endpoint = provider switch
6566
{
66-
Providers.Free or Providers.OpenRouter => new Uri(tryAGI.OpenAI.CustomProviders.OpenRouterBaseUrl),
67+
Provider.Free or Provider.OpenRouter => new Uri(tryAGI.OpenAI.CustomProviders.OpenRouterBaseUrl),
6768
_ => null,
6869
};
6970
model = model switch
@@ -77,9 +78,9 @@ public static IChatClient GetChatModel(
7778
};
7879
var apiKey = provider switch
7980
{
80-
Providers.OpenAi or null => Environment.GetEnvironmentVariable("OPENAI_API_KEY") ??
81+
Provider.OpenAi or null => Environment.GetEnvironmentVariable("OPENAI_API_KEY") ??
8182
throw new InvalidOperationException("OPENAI_API_KEY environment variable is not set."),
82-
Providers.OpenRouter or Providers.Free => Environment.GetEnvironmentVariable("OPENROUTER_API_KEY") ??
83+
Provider.OpenRouter or Provider.Free => Environment.GetEnvironmentVariable("OPENROUTER_API_KEY") ??
8384
throw new InvalidOperationException("OPENROUTER_API_KEY environment variable is not set."),
8485
_ => throw new NotImplementedException(),
8586
};

src/Cli/src/Models/Format.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace LangChain.Cli;
2+
3+
internal enum Format
4+
{
5+
Text,
6+
Lines,
7+
Json,
8+
Markdown,
9+
ConventionalCommit,
10+
}

src/Cli/src/Models/Provider.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace LangChain.Cli.Models;
2+
3+
internal enum Provider
4+
{
5+
OpenAi,
6+
OpenRouter,
7+
Anthropic,
8+
Free,
9+
}

src/Cli/src/Models/Tool.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace LangChain.Cli.Models;
2+
3+
internal enum Tool
4+
{
5+
Filesystem,
6+
Fetch,
7+
GitHub,
8+
Git,
9+
Puppeteer,
10+
SequentialThinking,
11+
Slack,
12+
}

0 commit comments

Comments
 (0)