Skip to content

Commit d17e1b4

Browse files
authored
feat: Preparations for code-execution agents (#120)
- Docker does not upload files, but attaches to a directory now. This allows to work with multiple files - Added test for c# project generation
1 parent 27e268b commit d17e1b4

File tree

7 files changed

+138
-43
lines changed

7 files changed

+138
-43
lines changed

src/libs/Extensions/LangChain.Extensions.Docker/Chain.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ public static class Chain
1616
/// <param name="inputKey"></param>
1717
/// <param name="outputKey"></param>
1818
/// <returns></returns>
19-
public static BaseStackableChain RunCodeInDocker(string image= "python:3.10", string filename = "main.py", string command = "python", string inputKey = "code", string outputKey = "result")
19+
public static DockerChain RunCodeInDocker(string image= "python:3.10", string arguments = "main.py", string command = "python", string? attachVolume=null, string outputKey = "result")
2020
{
21-
return new DockerChain(image, filename,command, inputKey, outputKey);
21+
return new DockerChain(image, arguments, command, attachVolume, outputKey);
2222
}
2323
}

src/libs/Extensions/LangChain.Extensions.Docker/DockerChain.cs

+20-38
Original file line numberDiff line numberDiff line change
@@ -29,45 +29,35 @@ public void Report(JSONMessage value)
2929
/// <summary>
3030
///
3131
/// </summary>
32-
public string Filename { get; }
32+
public string Arguments { get; }
3333

3434
/// <summary>
3535
///
3636
/// </summary>
3737
public string Command { get; }
3838

39+
public string? AttachVolume { get; }
40+
3941
/// <summary>
4042
///
4143
/// </summary>
4244
/// <param name="image"></param>
43-
/// <param name="filename"></param>
45+
/// <param name="arguments"></param>
4446
/// <param name="command"></param>
45-
/// <param name="inputKey"></param>
4647
/// <param name="outputKey"></param>
47-
public DockerChain(string image= "python:3", string filename="main.py", string command="python", string inputKey="code",string outputKey="result")
48+
public DockerChain(string image= "python:3", string arguments="main.py", string command="python", string? attachVolume=null, string outputKey="result")
4849
{
4950
Image = image;
50-
Filename = filename;
51+
Arguments = arguments;
5152
Command = command;
52-
InputKeys = new[] {inputKey};
53+
AttachVolume = attachVolume;
5354
OutputKeys = new[] {outputKey};
5455

5556
using var configuration = new DockerClientConfiguration();
5657
_client = configuration.CreateClient();
5758
}
5859

59-
private static string SanitizeCode(string code)
60-
{
61-
if (code.StartsWith("```", StringComparison.Ordinal))
62-
{
63-
// remove first and last lines
64-
var lines = code.Split("\n");
65-
var res = string.Join("\n", lines[1..^1]);
66-
return res;
67-
}
68-
return code;
69-
}
70-
60+
7161
/// <summary>
7262
///
7363
/// </summary>
@@ -82,37 +72,29 @@ await _client.Images.CreateImageAsync(new ImagesCreateParameters()
8272
FromImage = Image
8373
}, null, new SuppressProgress(), CancellationToken.None).ConfigureAwait(false);
8474

75+
var binds = new List<string>();
8576

86-
var code = SanitizeCode(values.Value[InputKeys[0]].ToString() ?? string.Empty);
87-
88-
89-
var tempDir = Path.GetTempPath();
90-
tempDir = Path.Combine(tempDir, Guid.NewGuid().ToString());
91-
var appDir = Path.Combine(tempDir, "app");
92-
Directory.CreateDirectory(appDir);
93-
var tempFile = Path.Combine(appDir, Filename);
94-
await File.WriteAllTextAsync(tempFile, code).ConfigureAwait(false);
95-
96-
MemoryStream archiveStream = new MemoryStream();
97-
await TarFile.CreateFromDirectoryAsync(tempDir,archiveStream,false).ConfigureAwait(false);
98-
archiveStream.Seek(0, SeekOrigin.Begin);
9977

100-
Directory.Delete(tempDir,true);
78+
if (AttachVolume != null)
79+
{
80+
var absolutePath = Path.GetFullPath(AttachVolume).Replace("\\","/").Replace(":","");
81+
binds.Add($"/{absolutePath}:/app");
82+
}
10183

10284
var container = await _client.Containers.CreateContainerAsync(new CreateContainerParameters()
10385
{
10486

10587
Image = Image,
106-
Cmd = new[] {Command,Filename},
88+
Cmd = new[] {Command,Arguments},
10789
WorkingDir = "/app",
90+
HostConfig = new HostConfig()
91+
{
92+
Binds = binds
93+
}
10894

10995
}).ConfigureAwait(false);
11096

111-
await _client.Containers.ExtractArchiveToContainerAsync(container.ID, new ContainerPathStatParameters()
112-
{
113-
AllowOverwriteDirWithFile = true,
114-
Path = "/",
115-
}, archiveStream, CancellationToken.None).ConfigureAwait(false);
97+
11698

11799
await _client.Containers.StartContainerAsync(container.ID, null).ConfigureAwait(false);
118100

src/libs/LangChain.Core/Chains/Chain.cs

+7
Original file line numberDiff line numberDiff line change
@@ -293,4 +293,11 @@ public static CrewChain Crew(
293293
{
294294
return new CrewChain(allAgents, manager, inputKey, outputKey);
295295
}
296+
297+
public static ExtractCodeChain ExtractCode(
298+
string inputKey = "text",
299+
string outputKey = "code")
300+
{
301+
return new ExtractCodeChain(inputKey, outputKey);
302+
}
296303
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System.Text;
2+
using LangChain.Abstractions.Schema;
3+
using LangChain.Chains.HelperChains;
4+
5+
namespace LangChain.Chains.StackableChains;
6+
7+
public class ExtractCodeChain: BaseStackableChain
8+
{
9+
public ExtractCodeChain(string inputKey="text", string outputKey="code")
10+
{
11+
InputKeys = new[] {inputKey};
12+
OutputKeys = new[] {outputKey};
13+
}
14+
15+
string ExtractCode(string md)
16+
{
17+
var lines = md.Split('\n');
18+
var code = new StringBuilder();
19+
var inCode = false;
20+
foreach (var line in lines)
21+
{
22+
if (line.StartsWith("```"))
23+
{
24+
inCode = !inCode;
25+
continue;
26+
}
27+
28+
if (inCode)
29+
{
30+
code.AppendLine(line);
31+
}
32+
}
33+
34+
if (code.Length == 0)
35+
{
36+
code.AppendLine(md);
37+
}
38+
39+
return code.ToString();
40+
41+
}
42+
43+
44+
protected override Task<IChainValues> InternalCall(IChainValues values)
45+
{
46+
47+
48+
if (values.Value[InputKeys[0]] is string text)
49+
{
50+
values.Value[OutputKeys[0]] = ExtractCode(text);
51+
}
52+
else
53+
{
54+
throw new InvalidOperationException($"Input key {InputKeys[0]} must be string");
55+
}
56+
57+
return Task.FromResult(values);
58+
}
59+
}

src/tests/LangChain.Providers.LLamaSharp.IntegrationTests/DockerTests.cs

+6-3
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,21 @@ public class DockerTests
1515
public void SimpleHelloWorldTest()
1616
{
1717
var model = LLamaSharpModelInstruction.FromPath(ModelPath);
18+
model.Configuration.AntiPrompts = new[] { "[END]" };
1819

1920
var promptText =
20-
@"Generate a python program that prints ""Hello world"" do not explain or comment the code. I expect only generated code from you.
21+
@"Generate a python program that prints ""Hello world"" do not explain or comment the code. I expect only generated code from you. Print [END] after you are done.
2122
2223
Generated code:";
2324

2425
var chain = Template(promptText, outputKey:"prompt")
2526
| LLM(model, inputKey:"prompt", outputKey:"code")
26-
| RunCodeInDocker(inputKey:"code", outputKey:"result");
27+
| ExtractCode("code","data")
28+
| SaveIntoFile("main.py")
29+
| RunCodeInDocker(attachVolume:"./", outputKey:"result");
2730
var res = chain.Run().Result;
2831

2932
Assert.AreEqual("Hello world", res.Value["result"].ToString()?.Trim());
3033
}
31-
34+
3235
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Diagnostics;
2+
3+
namespace LangChain.Providers.Ollama.IntegrationTests;
4+
using static LangChain.Chains.Chain;
5+
using static LangChain.Extensions.Docker.Chain;
6+
[TestFixture]
7+
public class DockerTests
8+
{
9+
[Test]
10+
[Explicit]
11+
public void SimpleHelloWorldTest()
12+
{
13+
var model = new OllamaLanguageModelInstruction("deepseek-coder:6.7b-instruct",options:new OllamaLanguageModelOptions()
14+
{
15+
Temperature = 0.0f,
16+
Stop = new[] { "[END]"}
17+
});
18+
19+
20+
var promptText =
21+
@"Generate a C# program that prints ""Hello, Anti!"". Do not explain the code. Print [END] after code is generated.
22+
Code:
23+
";
24+
25+
Directory.CreateDirectory("test");
26+
CreateCSharpProject("test");
27+
28+
29+
var chain = Template(promptText, outputKey: "prompt")
30+
| LLM(model, inputKey: "prompt", outputKey: "code")
31+
| ExtractCode("code", "data")
32+
| SaveIntoFile("test\\Program.cs")
33+
| RunCodeInDocker(image: "mcr.microsoft.com/dotnet/sdk:8.0", command:"dotnet", arguments:"run",attachVolume: "./test", outputKey: "result");
34+
var res = chain.Run().Result;
35+
36+
Assert.AreEqual("Hello, Anti!", res.Value["result"].ToString()?.Trim());
37+
}
38+
39+
void CreateCSharpProject(string path)
40+
{
41+
Process.Start("dotnet", $"new console -o {path}").WaitForExit();
42+
}
43+
}

src/tests/LangChain.Providers.Ollama.IntegrationTests/LangChain.Providers.Ollama.IntegrationTests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
</ItemGroup>
1111

1212
<ItemGroup>
13+
<ProjectReference Include="..\..\libs\Extensions\LangChain.Extensions.Docker\LangChain.Extensions.Docker.csproj" />
1314
<ProjectReference Include="..\..\libs\Providers\LangChain.Providers.Ollama\LangChain.Providers.Ollama.csproj" />
1415
</ItemGroup>
1516

0 commit comments

Comments
 (0)