From ed902b0f5753dd593ba9c0f080afe73a35ea0a31 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 29 Sep 2025 17:58:00 -0500 Subject: [PATCH] add package handling --- .../Functions/PyProgrammerFn.cs | 4 +- .../Helpers/PyPackageHelper.cs | 178 ++++++++++++++++++ .../LlmContext/LlmContextIn.cs | 5 +- .../LlmContext/LlmContextOut.cs | 3 + .../Models/PackageInstallResult.cs | 7 + .../Using.cs | 2 + ...il-code-python_generate_instruction.liquid | 20 +- src/WebStarter/appsettings.json | 5 +- 8 files changed, 215 insertions(+), 9 deletions(-) create mode 100644 src/Plugins/BotSharp.Plugin.PythonInterpreter/Helpers/PyPackageHelper.cs create mode 100644 src/Plugins/BotSharp.Plugin.PythonInterpreter/Models/PackageInstallResult.cs diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Functions/PyProgrammerFn.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Functions/PyProgrammerFn.cs index c44bf5cf..477778a0 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Functions/PyProgrammerFn.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Functions/PyProgrammerFn.cs @@ -108,7 +108,9 @@ public class PyProgrammerFn : IFunctionCallback } catch (Exception ex) { - _logger.LogError(ex, $"Error when executing python code."); + var errorMsg = $"Error when executing python code."; + message.Content = $"{errorMsg} {ex.Message}"; + _logger.LogError(ex, errorMsg); } return true; diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Helpers/PyPackageHelper.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Helpers/PyPackageHelper.cs new file mode 100644 index 00000000..2c8cfd3c --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Helpers/PyPackageHelper.cs @@ -0,0 +1,178 @@ +using System.Diagnostics; +using System.Threading.Tasks; + +namespace BotSharp.Plugin.PythonInterpreter.Helpers; + +internal static class PyPackageHelper +{ + /// + /// Install python packages + /// + /// + /// + internal static async Task InstallPackages(List? packages) + { + if (packages.IsNullOrEmpty()) + { + return new PackageInstallResult { Success = true }; + } + + try + { + var packageList = string.Join(" ", packages); + var startInfo = new ProcessStartInfo + { + FileName = "pip", + Arguments = $"install {packageList}", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) + { + return new PackageInstallResult + { + Success = false, + ErrorMsg = "Failed to start pip process" + }; + } + + await process.WaitForExitAsync(); + var output = await process.StandardOutput.ReadToEndAsync(); + var error = await process.StandardError.ReadToEndAsync(); + + if (process.ExitCode == 0) + { + return new PackageInstallResult { Success = true }; + } + else + { + var errorMsg = $"Failed to install packages. Exit code: {process.ExitCode}. Error: {error}"; + return new PackageInstallResult + { + Success = false, + ErrorMsg = errorMsg + }; + } + } + catch (Exception ex) + { + var errorMsg = $"Exception occurred while installing packages: {ex.Message}"; + return new PackageInstallResult + { + Success = false, + ErrorMsg = errorMsg + }; + } + } + + /// + /// Get packages that are not installed + /// + /// + /// + public static async Task> GetUninstalledPackages(List? packages) + { + var missingPackages = new List(); + + if (packages.IsNullOrEmpty()) + { + return missingPackages; + } + + try + { + var installedPackages = await GetInstalledPackages(); + foreach (var package in packages) + { + // Check for common package name mappings + var mappedPackageName = MapToPackageName(package); + var isInstalled = installedPackages.Any(x => x.IsEqualTo(mappedPackageName)); + if (!isInstalled) + { + missingPackages.Add(mappedPackageName); + } + } + return missingPackages; + } + catch (Exception ex) + { + throw; + } + } + + + private static async Task> GetInstalledPackages() + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "pip", + Arguments = "list --format=freeze", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) + { + throw new InvalidOperationException("Failed to start pip process"); + } + + await process.WaitForExitAsync(); + var output = await process.StandardOutput.ReadToEndAsync(); + + if (process.ExitCode != 0) + { + var error = await process.StandardError.ReadToEndAsync(); + throw new InvalidOperationException($"pip list failed with exit code {process.ExitCode}: {error}"); + } + + // Parse pip list output (format: package==version) + var packages = output.Split("\n", StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Split("==", StringSplitOptions.None)[0].Trim().ToLowerInvariant()) + .Where(pkg => !string.IsNullOrEmpty(pkg)) + .ToList(); + return packages; + } + catch (Exception ex) + { + throw; + } + } + + /// + /// Map common import name to actual package name + /// + /// + /// + private static string MapToPackageName(string importName) + { + var packageMappings = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "cv2", "opencv-python" }, + { "PIL", "Pillow" }, + { "sklearn", "scikit-learn" }, + { "yaml", "PyYAML" }, + { "bs4", "beautifulsoup4" }, + { "dateutil", "python-dateutil" }, + { "serial", "pyserial" }, + { "psutil", "psutil" }, + { "requests", "requests" }, + { "numpy", "numpy" }, + { "pandas", "pandas" }, + { "matplotlib", "matplotlib" }, + { "scipy", "scipy" }, + { "seaborn", "seaborn" }, + { "plotly", "plotly" } + }; + + return packageMappings.TryGetValue(importName, out var actualName) ? actualName : importName; + } +} diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/LlmContext/LlmContextIn.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/LlmContext/LlmContextIn.cs index bcac0e8b..0f761912 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/LlmContext/LlmContextIn.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/LlmContext/LlmContextIn.cs @@ -5,5 +5,8 @@ namespace BotSharp.Plugin.PythonInterpreter.LlmContext; public class LlmContextIn { [JsonPropertyName("user_requirement")] - public string UserRquirement { get; set; } + public string UserRquirement { get; set; } = string.Empty; + + [JsonPropertyName("imported_packages")] + public List ImportedPackages { get; set; } = []; } diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/LlmContext/LlmContextOut.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/LlmContext/LlmContextOut.cs index b074a004..2af106ee 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/LlmContext/LlmContextOut.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/LlmContext/LlmContextOut.cs @@ -6,4 +6,7 @@ public class LlmContextOut { [JsonPropertyName("python_code")] public string PythonCode { get; set; } + + [JsonPropertyName("imported_packages")] + public List? ImportedPackages { get; set; } } diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Models/PackageInstallResult.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Models/PackageInstallResult.cs new file mode 100644 index 00000000..b6fdb994 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Models/PackageInstallResult.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Plugin.PythonInterpreter.Models; + +internal class PackageInstallResult +{ + internal bool Success { get; set; } + internal string ErrorMsg { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Using.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Using.cs index bf94142e..73d0bc86 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Using.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Using.cs @@ -28,3 +28,5 @@ global using BotSharp.Plugin.PythonInterpreter.LlmContext; global using BotSharp.Plugin.PythonInterpreter.Settings; global using BotSharp.Plugin.PythonInterpreter.Functions; global using BotSharp.Plugin.PythonInterpreter.Hooks; +global using BotSharp.Plugin.PythonInterpreter.Models; +global using BotSharp.Plugin.PythonInterpreter.Helpers; diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-code-python_generate_instruction.liquid b/src/Plugins/BotSharp.Plugin.PythonInterpreter/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-code-python_generate_instruction.liquid index 3ac455a1..91da2bb5 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-code-python_generate_instruction.liquid +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-code-python_generate_instruction.liquid @@ -8,11 +8,12 @@ You must strictly follow the "Hard Requirements", "Code Requirements", and "Resp ***** Hard Requirements ***** -1. Your output python code must be well constructed inside one or multiple functions. -2. You must not include any code, explanations, or comments outside these functions. -3. Do not include explanations, comments outside code. -4. You must use print() once to output the final result only. Do not print intermediate values, logs, or debug info. -5. If randomness is required, set a fixed seed inside a function. +1. You must import packages at the beginning of the code. DO NOT include any import statement in the middle of the functions. +2. Your output python code must be well constructed inside one or multiple functions. +3. You must not include any code, explanations, or comments outside these functions. +4. Do not include explanations, comments outside code. +5. You must use print() once to output the final result only. Do not print intermediate values, logs, or debug info. +6. If randomness is required, set a fixed seed inside a function. ***** Code Requirements ***** @@ -26,14 +27,21 @@ You must strictly follow the "Hard Requirements", "Code Requirements", and "Resp c). Except the main() function, it is preferable to generate functions that accept parameters and return values. 3. Error handling a). If necessary, use try/except block inside main() to catch any errors and produce a single final printed message. + b). Do not apply try/except block for package import. 4. Output a). You must print the final result once in main(). 5. Compatibility a). You must keep the code compatible with "Python {{ python_version }}" +6. Package Usage + a). Prefer using Python standard library modules when possible (os, sys, json, csv, math, statistics, datetime, etc.) + b). Only use external packages (like pandas, numpy, matplotlib) when absolutely necessary for the task + c). If using external packages, provide fallback implementations using standard library when feasible + d). Always include external packages in the "imported_packages" list in your response ***** Response Format ***** You must output the response in the following JSON format: { - "python_code": "The python code that can fulfill user's request." + "python_code": "The python code that can fulfill user's request.", + "imported_packages": a list of strings that contains the packages that are imported in the code you generated. } \ No newline at end of file diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 415662d0..c2a1974a 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -567,7 +567,10 @@ "PythonInterpreter": { "InstallLocation": "C:/Users/xxx/AppData/Local/Programs/Python/Python313/python313.dll", - "PythonVersion": "3.13", + "PythonVersion": "3.13.3", + "AutoInstallPackages": false, + "AllowedPackages": [ "pandas", "numpy", "matplotlib", "requests", "beautifulsoup4", "scipy", "seaborn", "plotly" ], + "EnableFallbackGeneration": true, "CodeGeneration": { "LlmProvider": "openai", "LlmModel": "gpt-5",