Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Robch/2410 oct19 line editing built in functions #338

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ideas/hf-instructions
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
I want a new helper function that can run any external CLI it wants, like git, grep, ch, etc... it should capture the output both std and err and return that as a string. if there's a failure, return relevant information as a string instead so the caller can figure out what to do to fix it.

Some CLIs will have different parameters depending on if they're running on windows or linux. you should also make a function that returns if we're running on windows or linux. that way the caller of the previous helper function can do the right thing.

You might want to make not in the helper function description in the first function that they should check if they're on windows or linux first before calling it. One key thing that's different, often, is that arguments have either `'` or `"` wrapping them.
62 changes: 59 additions & 3 deletions src/ai/.x/templates/helper_functions/.ai/prompt.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,67 @@
## On your profile and general capabilities:
- Your logic and reasoning should be rigorous and intelligent.
- You **must always** select one or more API Names to call to satisfy requests.
- You prefer action to words; just do the task, don't tell me about it.

# Scenario context: Helper functions
"Helper functions" are C# public static methods that are attributed using the `HelperFunctionDescription` attribute. They are used to provide additional functionality to the CLI.

You **MUST ALWAYS** follow the rules for writing helper functions:
1. You **MUST ALWAYS** only use acceptable data types for helper function parameters and return values. Acceptable data types (`T`) are:
- Plain old data types (those represented by C#'s `TypeCode` enumeration)
- `List<T>` where T is any acceptable data type.
- `Array<T>` where T is any acceptable data type.
- `Tuple<T>`, `Tuple<T, T>` where T is any acceptable data type (and all T's are the same type).
- This means that `Tuple<int, string>` is not an acceptable data type, but `Tuple<int, int>` is.

2. You **MUST ALWAYS** apply a `HelperFunctionDescription` attribute to helper functions.

## Example helper function
```csharp
using Azure.AI.Details.Common.CLI.Extensions.HelperFunctions;

public static class UserNameHelperFunctionClass
{
[HelperFunctionDescription("Gets the user's name")]
public static string GetUsersName()
{
return Environment.UserName;
}
}
```

## Rules for writing new code
- You **MUST ALWAYS** write complete source files, not snippets, no placeholders, and no TODO comments.
- You **MUST ALWAYS** write code that compiles and runs without errors, with no additional work.
- You **MUST ALWAYS** write code that is well-formatted and easy to read.
- You **MUST ALWAYS** write code that is well-documented and easy to understand.
- You **MUST ALWAYS** use descriptive names for classes, methods, and variables, specific to the task.
- You **MUST ALWAYS** carefully escape source code before calling APIs provided to write files to disk, including double quoted strings that look like: `$"..."`; those must be turned into `$\"...\"`.

## Rules for writing new code that requires a package reference

If you write code that uses nuget packages, you **MUST ALWAYS** add the necessary package references to the `HelperFunctionsProject.csproj` file. If you did, you must follow these rules:
1. You **MUST** read `HelperFunctionsProject.csproj` file.
2. You **MUST** add new Nuget package references to the `HelperFunctionsProject.csproj` file.
3. Do **NOT** remove anything from the `HelperFunctionsProject.csproj` file that was already there.

NOTE: Only do this for "external" nuget packages. If you're just using features from dotnet core libraries, you don't need to add anything to the `HelperFunctionsProject.csproj` file.

## Rules for writing files or creating directories
- You **MUST ALWAYS** write new classes into new files on disk using APIs provided.
- You **MUST ALWAYS** use filenames that match the class name.
- You **must never** create new directories.

## Scenario specifics

I will now tell you about a scenario I want to make a helper function for:

---
{instructions}
---

Task you must perform:
1. Please create a new class with these helper functions. Do it now.
2. Please update the csproj file to include any new references required. Do it now.
1. Please create a new class with this/these helper function(s). Do it now.
2. If required, update the `HelperFunctionsProject.csproj` file with the necessary package references. Do it now.

Don't show me the code, just create the files. Do it now.
Don't show me the code, just create the files. Do it now.
50 changes: 0 additions & 50 deletions src/ai/.x/templates/helper_functions/.ai/system.md
Original file line number Diff line number Diff line change
@@ -1,51 +1 @@
You are an AI assistant that writes code for C# developers.

## On your profile and general capabilities:
- Your logic and reasoning should be rigorous and intelligent.
- You **must always** select one or more API Names to call to satisfy requests.
- You prefer action to words; just do the task, don't tell me about it.

# Scenario context: Helper functions
"Helper functions" are C# public static methods that are attributed using the `HelperFunctionDescription` attribute. They are used to provide additional functionality to the CLI.

You **must always** follow the rules for writing helper functions:
1. You **must always** only use acceptable data types for helper function parameters and return values. Acceptable data types (`T`) are:
- Plain old data types (those represented by C#'s `TypeCode` enumeration)
- `List<T>` where T is any acceptable data type.
- `Array<T>` where T is any acceptable data type.
- `Tuple<T>`, `Tuple<T, T>` where T is any acceptable data type (and all T's are the same type).
- This means that `Tuple<int, string>` is not an acceptable data type, but `Tuple<int, int>` is.

2. You **must always** apply a `HelperFunctionDescription` attribute to helper functions.

## Example helper function
```csharp
using Azure.AI.Details.Common.CLI.Extensions.HelperFunctions;

public static class UserNameHelperFunctionClass
{
[HelperFunctionDescription("Gets the user's name")]
public static string GetUsersName()
{
return Environment.UserName;
}
}
```

## Rules for writing new code
- You **must always** write complete source files, not snippets, no placeholders, and no TODO comments.
- You **must always** write code that compiles and runs without errors, with no additional work.
- You **must always** write code that is well-formatted and easy to read.
- You **must always** write code that is well-documented and easy to understand.
- You **must always** use descriptive names for classes, methods, and variables, specific to the task.
- You **must always** carefully escape source code before calling APIs provided to write files to disk, including double quoted strings that look like: `$"..."`; those must be turned into `$\"...\"`.

## Rules for writing new code that requires a package reference
1. You **must always** read `HelperFunctionsProject.csproj` file.
2. You **must always** update `HelperFunctionsProject.csproj` project file with the package reference.
3. You **must always** save the complete `HelperFunctionsProject.csproj` file back to disk.

## Rules for writing files or creating directories
- You **must always** write new classes into new files on disk using APIs provided.
- You **must always** use filenames that match the class name.
- You **must never** create new directories.
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,46 @@ public static bool CreateFileAndSaveText(string fileName, string text)
return true;
}

[HelperFunctionDescription("Appends text to a file; if the file does not exist, it is created")]
public static bool AppendTextToFile(string fileName, string text)
[HelperFunctionDescription("Opens a text file for line editing; returns the file content as a string with line numbers")]
public static string OpenTextFileForLineEditing(string fileName)
{
FileHelpers.AppendAllText(fileName, text, new UTF8Encoding(false));
return true;
return FileLineEditorHelpers.OpenFileForLineEditing(fileName);
}

[HelperFunctionDescription("Updates a line in a file previously opened for line editing; line numbers must be original line numbers returned from OpenTextFileForLineEditing")]
public static string UpdateLineInFile(string fileName, int lineNumber, string newText)
{
return FileLineEditorHelpers.UpdateLineInFile(fileName, lineNumber, newText);
}

[HelperFunctionDescription("Removes lines from a file previously opened for line editing; line numbers must be original line numbers returned from OpenTextFileForLineEditing")]
public static string RemoveLinesFromFile(string fileName, int firstLineToRemove, int lastLineToRemove)
{
return FileLineEditorHelpers.RemoveLinesFromFile(fileName, firstLineToRemove, lastLineToRemove);
}

[HelperFunctionDescription("Inserts lines before a line in a file previously opened for line editing; line numbers must be original line numbers returned from OpenTextFileForLineEditing")]
public static string InsertLinesIntoFileBeforeLine(string fileName, string text, int lineNumber)
{
return FileLineEditorHelpers.InsertLinesIntoFileBeforeOrAfterLine(fileName, text, lineNumber, true, false);
}

[HelperFunctionDescription("Inserts lines after a line in a file previously opened for line editing; line numbers must be original line numbers returned from OpenTextFileForLineEditing")]
public static string InsertLinesIntoFileAfterLine(string fileName, string text, int lineNumber)
{
return FileLineEditorHelpers.InsertLinesIntoFileBeforeOrAfterLine(fileName, text, lineNumber, false, true);
}

[HelperFunctionDescription("Moves a block of lines before a line in a file previously opened for line editing; line numbers must be original line numbers returned from OpenTextFileForLineEditing")]
public static string MoveLinesBeforeLine(string fileName, int firstLineToMove, int lastLineToMove, int insertBeforeLineNumber)
{
return FileLineEditorHelpers.MoveLineBlockBeforeOrAfterLine(fileName, firstLineToMove, lastLineToMove, insertBeforeLineNumber, true, false);
}

[HelperFunctionDescription("Moves a block of lines after a line in a file previously opened for line editing; line numbers must be original line numbers returned from OpenTextFileForLineEditing")]
public static string MoveLinesAfterLine(string fileName, int firstLineToMove, int lastLineToMove, int insertAfterLineNumber)
{
return FileLineEditorHelpers.MoveLineBlockBeforeOrAfterLine(fileName, firstLineToMove, lastLineToMove, insertAfterLineNumber, false, true);
}

[HelperFunctionDescription("Creates a directory if it doesn't already exist")]
Expand Down
149 changes: 149 additions & 0 deletions src/extensions/helper_functions_extension/FileLineEditorHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
//

using System.Text;

namespace Azure.AI.Details.Common.CLI.Extensions.HelperFunctions
{
public static class FileLineEditorHelpers
{
public static string OpenFileForLineEditing(string fileName)
{
var content = FileHelpers.FileExists(fileName)
? FileHelpers.ReadAllText(fileName, new UTF8Encoding(false))
: null;
if (content == null) return $"File not found: {fileName}";

var linePairs = new List<KeyValuePair<int, string?>>();
_lineEditableFiles[fileName] = linePairs;

var lines = content.Split('\n')
.Select(x => x.Trim('\r'))
.ToList();
for (var i = 0; i < lines.Count; i++)
{
linePairs.Add(new KeyValuePair<int, string?>(i + 1, lines[i]));
}

return string.Join(Environment.NewLine, linePairs.Select(p => $"{p.Key}: {p.Value}"));
}

public static string UpdateLineInFile(string fileName, int lineNumber, string newText)
{
if (!_lineEditableFiles.ContainsKey(fileName)) return $"File not opened for line editing: {fileName}; use OpenTextFileForLineEditing() first";

if (newText.Contains('\n'))
{
return "UpdateLineInFile does not support multi-line changes; use a combination of RemoveLinesFromFile and InsertLinesIntoFileBeforeOrAfterLine instead";
}

var linePairs = _lineEditableFiles[fileName];
var lineIndex = linePairs.FindIndex(x => x.Key == lineNumber);
if (lineIndex < 0) return $"Line number {lineNumber} not found in file {fileName}; re-open the file for line editing";

linePairs[lineIndex] = new KeyValuePair<int, string?>(lineNumber, newText);

UpdateFileContent(fileName, linePairs);
return $"{lineNumber}: {newText}";
}

public static string MoveLineBlockBeforeOrAfterLine(string fileName, int firstLineNumberToMove, int lastLineNumberToMove, int insertBeforeOrAfterLineNumber, bool insertBefore, bool insertAfter)
{
if (!_lineEditableFiles.ContainsKey(fileName)) return $"File not opened for line editing: {fileName}; use OpenTextFileForLineEditing() first";

var linePairs = _lineEditableFiles[fileName];
var firstToMove = linePairs.FindIndex(x => x.Key == firstLineNumberToMove);
if (firstToMove < 0) return $"Line number {firstLineNumberToMove} not found in file {fileName}; re-open the file for line editing";

var lastToMove = linePairs.FindIndex(x => x.Key == lastLineNumberToMove);
if (lastToMove < 0) return $"Line number {lastLineNumberToMove} not found in file {fileName}; re-open the file for line editing";

var moved = new List<KeyValuePair<int, string>>();
for (var i = firstToMove; i <= lastToMove; i++)
{
var line = linePairs[i];
linePairs[i] = new KeyValuePair<int, string?>(line.Key, null);
if (line.Value != null) moved.Add(line!);
}

var insertionPoint = linePairs.FindIndex(x => x.Key == insertBeforeOrAfterLineNumber);
if (insertionPoint < 0) return $"Line number {insertBeforeOrAfterLineNumber} not found in file {fileName}; re-open the file for line editing";

var pairsPart1 = insertBefore
? linePairs.Take(insertionPoint).ToList()
: linePairs.Take(insertionPoint + 1).ToList();
var insertPairs = moved
.Select(x => new KeyValuePair<int, string>(-1, x.Value))
.ToList();
var pairsPart2 = insertAfter
? linePairs.Skip(insertionPoint + 1).ToList()
: linePairs.Skip(insertionPoint).ToList();

var allPairs = pairsPart1!.Concat(insertPairs).Concat(pairsPart2!).ToList();
_lineEditableFiles[fileName] = allPairs!;

return UpdateFileContent(fileName, allPairs!);
}

public static string RemoveLinesFromFile(string fileName, int firstLineToRemove, int lastLineToRemove)
{
if (!_lineEditableFiles.ContainsKey(fileName)) return $"File not opened for line editing: {fileName}; use OpenTextFileForLineEditing() first";

var linePairs = _lineEditableFiles[fileName];
var firstToRemove = linePairs.FindIndex(x => x.Key == firstLineToRemove);
if (firstToRemove < 0) return $"Line number {firstLineToRemove} not found in file {fileName}; re-open the file for line editing";

var lastToRemove = linePairs.FindIndex(x => x.Key == lastLineToRemove);
if (lastToRemove < 0) return $"Line number {lastLineToRemove} not found in file {fileName}; re-open the file for line editing";

var removed = new List<KeyValuePair<int, string?>>();
for (var i = firstToRemove; i <= lastToRemove; i++)
{
var line = linePairs[i];
linePairs[i] = new KeyValuePair<int, string?>(line.Key, null);
removed.Add(line);
}

return UpdateFileContent(fileName, linePairs);
}

public static string InsertLinesIntoFileBeforeOrAfterLine(string fileName, string text, int lineNumber, bool insertBefore, bool insertAfter)
{
if (!_lineEditableFiles.ContainsKey(fileName)) return $"File not opened for line editing: {fileName}; use OpenTextFileForLineEditing() first";
if (insertBefore && insertAfter) throw new ArgumentException("Only one of insertBefore and insertAfter can be true");

var linePairs = _lineEditableFiles[fileName];
var insertionPoint = linePairs.FindIndex(x => x.Key == lineNumber);
if (insertionPoint < 0) return $"Line number {lineNumber} not found in file {fileName}; re-open the file for line editing";

var pairsPart1 = insertBefore
? linePairs.Take(insertionPoint).ToList()
: linePairs.Take(insertionPoint + 1).ToList();
var insertPairs = text.Split('\n')
.Select(x => x.Trim('\r'))
.Select(x => new KeyValuePair<int, string>(-1, x))
.ToList();
var pairsPart2 = insertAfter
? linePairs.Skip(insertionPoint + 1).ToList()
: linePairs.Skip(insertionPoint).ToList();

var allPairs = pairsPart1!.Concat(insertPairs).Concat(pairsPart2!).ToList();
_lineEditableFiles[fileName] = allPairs!;

return UpdateFileContent(fileName, allPairs!);
}

private static string UpdateFileContent(string fileName, List<KeyValuePair<int, string?>> linePairs)
{
var keepNonNull = linePairs.Where(p => p.Value != null).ToList();
var newContent = string.Join(Environment.NewLine, keepNonNull.Select(p => p.Value));
FileHelpers.WriteAllText(fileName, newContent, new UTF8Encoding(false));

return $"Updated `{fileName}`:\n{OpenFileForLineEditing(fileName)}";
}

private static Dictionary<string, List<KeyValuePair<int, string?>>> _lineEditableFiles = new();
}
}
Loading
Loading