firstaid

package
v0.0.0-...-b70ad06 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 7, 2024 License: MIT Imports: 26 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ListFiles = tools.Func(
	"List files",
	"Lists some of the contents in the specified directory. Don't use this on files. Don't use a depth higher than 2 unless you're really sure.",
	"list_files",
	func(r tools.Runner, p ListFilesParams) tools.Result {
		if p.Depth < 1 {
			p.Depth = 1
		}
		p.Path = expandPath(p.Path)

		items := make(map[string]FileInfo)
		entries := 0

		err := filepath.WalkDir(p.Path, func(path string, d os.DirEntry, err error) error {
			if err != nil {
				return err
			}
			relPath, _ := filepath.Rel(p.Path, path)
			if relPath == "." {

				return nil
			}
			depth := len(strings.Split(relPath, string(os.PathSeparator)))
			if depth > p.Depth {
				return filepath.SkipDir
			}
			entries++
			if entries >= 1_000 {
				if d.IsDir() {
					return filepath.SkipDir
				} else {
					return nil
				}
			}
			if d.IsDir() {
				subItems, _ := os.ReadDir(path)
				items[relPath] = FileInfo{
					Type:            "directory",
					Count:           len(subItems),
					ContentsSkipped: depth == p.Depth,
				}
				switch d.Name() {
				case ".git", "node_modules":
					return filepath.SkipDir
				}
			} else {
				file, _ := os.Open(path)
				scanner := bufio.NewScanner(file)
				lines := 0
				for scanner.Scan() {
					lines++
				}
				items[relPath] = FileInfo{
					Type:  "file",
					Lines: lines,
				}
				file.Close()
			}
			return nil
		})
		label := fmt.Sprintf("List files in `%s`", p.Path)
		if err != nil {
			return tools.Error(label, err)
		}
		result := map[string]any{
			"items":        items,
			"totalEntries": entries,
		}
		if entries > 1_000 {
			result["note"] = fmt.Sprintf("There were %d entries, but we could only include 1000.", entries)
		}
		return tools.Success(label, result)
	},
)
View Source
var LookAtImage = tools.Func(
	"Look at image",
	"Displays an image from the specified path. Use this to view an image file.",
	"look_at_image",
	func(r tools.Runner, p LookAtImageParams) tools.Result {
		p.Path = expandPath(p.Path)
		label := fmt.Sprintf("Look at image `%s`", filepath.Base(p.Path))
		if _, err := os.Stat(p.Path); os.IsNotExist(err) {
			return tools.Error(label, fmt.Errorf("file does not exist: %s", p.Path))
		}
		var rb tools.ResultBuilder
		if err := rb.AddImage(p.Path, p.HighQuality); err != nil {
			return tools.Error(label, err)
		}
		content := map[string]string{
			"message": fmt.Sprintf("You will receive %s from the user as an automated message.", filepath.Base(p.Path)),
		}
		return rb.Success(label, content)
	},
)
View Source
var LookAtRealWorld = tools.Func(
	"Look at real world",
	"Takes a photo of the real world with the camera. Optionally pans/tilts first (camera has a 360 view). If you're looking for something, search by tilting/panning, taking a low-resolution image, and look at the result. If you don't see what you want, do another search pass, otherwise you can choose to get the high-resolution image if you want to see more.",
	"look_at_real_world",
	func(r tools.Runner, p LookAtRealWorldParams) tools.Result {
		device, err := onvif.NewDevice(onvif.DeviceParams{
			Xaddr:    os.Getenv("CAMERA_ONVIF"),
			Username: os.Getenv("CAMERA_USERNAME"),
			Password: os.Getenv("CAMERA_PASSWORD"),
		})
		if err != nil {
			return tools.Error("Look at real world", fmt.Errorf("failed to connect to camera: %v", err))
		}

		profile, err := getDefaultProfile(device)
		if err != nil {
			return tools.Error("Look at real world", fmt.Errorf("failed to get metadata about camera: %w", err))
		}

		if p.RelativePan != 0 || p.RelativeTilt != 0 {
			err := relativeMove(device, profile.Token, p.RelativePan, p.RelativeTilt, 0)
			if err != nil {
				return tools.Error("Look at real world", fmt.Errorf("failed to pan/tilt camera: %v", err))
			}
		}

		photoPath, err := takePhoto()
		if err != nil {
			return tools.Error("Look at real world", fmt.Errorf("failed to get photo path: %v", err))
		}
		defer os.Remove(photoPath)
		var rb tools.ResultBuilder
		rb.AddImage(photoPath, p.HighQuality)
		return rb.Success("Look at real world", fmt.Sprintf("You will receive the photo (%s) from the user as an automated message.", filepath.Base(photoPath)))
	},
)
View Source
var RunAppleScript = tools.Func(
	"Run AppleScript",
	"Run AppleScript (osascript) on the user's macOS and return the output",
	"run_apple_script",
	func(r tools.Runner, p RunAppleScriptParams) tools.Result {
		if len(p.ScriptLines) == 0 {
			return tools.Error("Run AppleScript failed", errors.New("missing script lines"))
		}
		// Run the shell command and capture the output or error.
		var args []string
		for _, line := range p.ScriptLines {
			args = append(args, "-e", line)
		}
		cmd := exec.Command("osascript", args...)
		output, err := cmd.CombinedOutput()
		if err != nil {
			return tools.Error(FirstLine(p.ScriptLines), fmt.Errorf("%w: %s", err, output))
		}
		return tools.Success(FirstLine(p.ScriptLines), map[string]any{
			"output": string(output),
		})
	})
View Source
var RunPowerShellCmd = tools.Func(
	"Run PowerShell command",
	"Run a shell command on the user's computer (a Windows machine) and return the output",
	"run_powershell_cmd",
	func(r tools.Runner, p RunPowerShellCmdParams) tools.Result {

		cmd := exec.Command("powershell", "-Command", p.Command)
		output, err := cmd.CombinedOutput()
		if err != nil {
			return tools.Error(p.Command, fmt.Errorf("%w: %s", err, output))
		}
		return tools.Success(p.Command, map[string]any{
			"output": string(output),
		})
	})
View Source
var RunPython = tools.Func(
	"Run Python",
	"Run Python on the user's computer and return the output",
	"run_python",
	func(r tools.Runner, p RunPythonParams) tools.Result {
		if len(p.Statements) == 0 {
			return tools.Error("Run Python failed", errors.New("missing Python statements"))
		}

		pythonExecutable := findPythonExecutable()
		if pythonExecutable == "" {
			return tools.Error("Run Python failed", errors.New("could not find Python executable"))
		}
		cmd := exec.Command(pythonExecutable)
		statementsJSON, err := json.Marshal(p.Statements)
		if err != nil {
			return tools.Error("Run Python failed", err)
		}
		cmd.Stdin = strings.NewReader(fmt.Sprintf(interpreterWrapper, statementsJSON))
		output, err := cmd.CombinedOutput()
		if err != nil {
			return tools.Error(FirstLine(p.Statements), fmt.Errorf("%w: %s", err, output))
		}
		return tools.Success(FirstLine(p.Statements), map[string]any{
			"output": string(output),
		})
	})
View Source
var RunShellCmd = tools.Func(
	"Run shell command",
	"Run a shell command on the user's computer and return the output",
	"run_shell_cmd",
	func(r tools.Runner, p RunShellCmdParams) tools.Result {

		cmd := exec.Command("sh", "-c", p.Command)
		output, err := cmd.CombinedOutput()
		if err != nil {
			return tools.Error(p.Command, fmt.Errorf("%w: %s", err, output))
		}
		if len(output) > 1_000 {

			tmpDstFile, err := os.CreateTemp("", "tmp-")
			if err != nil {
				return tools.Error(p.Command, err)
			}
			defer tmpDstFile.Close()
			_, err = tmpDstFile.Write(output)
			if err != nil {
				return tools.Error(p.Command, err)
			}
			return tools.Success(p.Command, map[string]any{
				"outputType": "file",
				"filePath":   tmpDstFile.Name(),
				"fileSize":   len(output),
				"firstLine":  FirstLineBytes(output),
				"note":       "The output was too long to fit here. It's been saved to a file. Prefer to immediately read the most relevant parts of this file instead of telling the user about it.",
			})
		}
		return tools.Success(p.Command, map[string]any{
			"outputType": "text",
			"output":     string(output),
		})
	})
View Source
var SliceFile = tools.Func(
	"Read file",
	"Read a slice of the lines in the specified file, if we imagine the file as a zero-indexed array of lines. Returns a JavaScript array value where each line is prefixed with its index in this imaginary array as a comment.",
	"slice_file",
	func(r tools.Runner, p SliceFileParams) tools.Result {
		p.Path = expandPath(p.Path)

		file, err := os.Open(p.Path)
		if err != nil {
			return tools.Error(p.Path, fmt.Errorf("failed to open file: %v", err))
		}
		defer file.Close()

		var lines []string
		scanner := bufio.NewScanner(file)
		for scanner.Scan() {
			lines = append(lines, scanner.Text())
		}
		if err := scanner.Err(); err != nil {
			return tools.Error(p.Path, err)
		}

		start := p.Start
		if start < 0 {
			start = len(lines) + start
		}
		if start < 0 {
			start = 0
		}

		end := len(lines)
		if p.End != nil {
			end = *p.End
		}
		if end > len(lines) {
			end = len(lines)
		}

		slicedLines := make([]map[string]string, 0, end-start)
		for i := start; i < end; i++ {
			line := lines[i]
			slicedLines = append(slicedLines, map[string]string{
				fmt.Sprintf("%d", i): line,
			})
		}
		remainingLines := len(lines) - end

		result := map[string]any{
			"filePath":       p.Path,
			"slicedLines":    slicedLines,
			"remainingLines": remainingLines,
		}

		var description string
		if end-start < 1 {
			return tools.Error(p.Path, fmt.Errorf("failed to read line %d from %q", start+1, p.Path))
		} else if start == end-1 {
			description = fmt.Sprintf("Read line %d from %q", start+1, p.Path)
		} else {
			description = fmt.Sprintf("Read lines %d-%d from %q", start+1, end, p.Path)
		}
		return tools.Success(description, result)
	})
View Source
var SpeakOutLoud = tools.Func(
	"Speak out loud",
	"Speak out loud to the user using TTS",
	"speak_out_loud",
	func(r tools.Runner, p SpeakOutLoudParams) tools.Result {
		tts.Speak(p.Message)
		numWords := len(reWords.FindAllString(p.Message, -1))
		return tools.Success(fmt.Sprintf("Spoke %d words", numWords), map[string]any{"success": true})
	})
View Source
var SpliceFile = tools.Func(
	"Update file",
	"Delete and/or replace a slice of the lines in the specified file, if we imagine the file as a zero-indexed array of lines.",
	"splice_file",
	func(r tools.Runner, p SpliceFileParams) tools.Result {
		p.Path = expandPath(p.Path)

		file, err := os.OpenFile(p.Path, os.O_RDWR|os.O_CREATE, 0644)
		if err != nil {
			return tools.Error(p.Path, fmt.Errorf("failed to open %q: %w", p.Path, err))
		}
		defer file.Close()

		var result strings.Builder

		scanner := bufio.NewScanner(file)
		var i int
		for {

			if i == p.Start {
				for _, line := range p.InsertLines {
					result.WriteString(line + "\n")
				}
			}
			if scanner.Scan() {

				if i < p.Start || i >= p.Start+p.DeleteCount {
					result.WriteString(scanner.Text() + "\n")
				}
			} else if err := scanner.Err(); err != nil {
				return tools.Error(p.Path, fmt.Errorf("failed to read file: %w", err))
			} else if i < p.Start {

				return tools.Error(p.Path, fmt.Errorf("file has less than %d lines", p.Start+1))
			} else {

				break
			}
			i++
		}

		if i > 0 {
			backupPath := fmt.Sprintf("%s.%d.bak", p.Path, time.Now().Unix())
			if err := copyFile(p.Path, backupPath); err != nil {
				return tools.Error(p.Path, fmt.Errorf("failed to create backup: %w", err))
			}
		}

		if err := writeFileAtomically(p.Path, strings.NewReader(result.String())); err != nil {
			return tools.Error(p.Path, fmt.Errorf("failed to write updated content: %w", err))
		}

		var description string
		var action string
		if p.DeleteCount > 0 && len(p.InsertLines) > 0 {
			if p.DeleteCount == len(p.InsertLines) {
				description = fmt.Sprintf("Replaced %s in %q", line(p.DeleteCount), p.Path)
				action = "replaced"
			} else {
				description = fmt.Sprintf("Replaced %s with %s in %q", line(p.DeleteCount), line(len(p.InsertLines)), p.Path)
				action = "replaced"
			}
		} else if p.DeleteCount > 0 {
			description = fmt.Sprintf("Deleted %s from %q", line(p.DeleteCount), p.Path)
			action = "deleted"
		} else if len(p.InsertLines) > 0 {
			description = fmt.Sprintf("Added %s to %q", line(len(p.InsertLines)), p.Path)
			action = "added"
		}

		return tools.Success(description, map[string]interface{}{
			"path":        p.Path,
			"action":      action,
			"deleteCount": p.DeleteCount,
			"insertCount": len(p.InsertLines),
		})
	})
View Source
var TakeScreenshot = tools.Func(
	"Take screenshot",
	"Takes a screenshot of the user's screen. Use this if the user refers to something you can't see.",
	"take_screenshot",
	func(r tools.Runner, params TakeScreenshotParams) tools.Result {

		screenshotPath := fmt.Sprintf("%s/screenshot_%d.png", os.TempDir(), time.Now().UnixNano())

		var cmd *exec.Cmd
		if runtime.GOOS == "windows" {

			cmd = exec.Command("powershell", "-command", fmt.Sprintf("Add-Type -AssemblyName System.Windows.Forms; $bmp = New-Object System.Drawing.Bitmap([System.Windows.Forms.SystemInformation]::VirtualScreen.Width, [System.Windows.Forms.SystemInformation]::VirtualScreen.Height); $graph = [System.Drawing.Graphics]::FromImage($bmp); $graph.CopyFromScreen([System.Windows.Forms.SystemInformation]::VirtualScreen.Location, [System.Drawing.Point]::Empty, $bmp.Size); $bmp.Save('%s');", screenshotPath))
		} else if runtime.GOOS == "darwin" {

			cmd = exec.Command("screencapture", "-x", screenshotPath)
		} else {
			return tools.Error("Take screenshot", fmt.Errorf("unsupported platform %s", runtime.GOOS))
		}
		output, err := cmd.CombinedOutput()
		if err != nil {
			return tools.Error("Take screenshot", fmt.Errorf("%w: %s", err, output))
		}
		defer os.Remove(screenshotPath)

		var rb tools.ResultBuilder
		if err := rb.AddImage(screenshotPath, true); err != nil {
			return tools.Error("Take screenshot", fmt.Errorf("failed to add image: %w", err))
		}

		fileName := filepath.Base(screenshotPath)
		return rb.Success("Take screenshot", map[string]any{
			"message":  fmt.Sprintf("You will receive %s from the user as an automated message.", fileName),
			"fileName": fileName,
		})
	},
)

Functions

func FirstLine

func FirstLine(lines []string) string

func FirstLineBytes

func FirstLineBytes(data []byte) string

func FirstLineString

func FirstLineString(s string) string

func RandomCatchphrase

func RandomCatchphrase() string

Types

type FileInfo

type FileInfo struct {
	Type            string `json:"type"`
	Lines           int    `json:"lines,omitempty"`
	Count           int    `json:"count,omitempty"`
	ContentsSkipped bool   `json:"contentsSkipped,omitempty"`
}

type ListFilesParams

type ListFilesParams struct {
	Path  string `json:"path"`
	Depth int    `json:"depth,omitempty"`
}

type LookAtImageParams

type LookAtImageParams struct {
	Path        string `json:"path"`
	HighQuality bool   `json:"high_quality,omitempty" description:"Use true if you want to see the image in higher resolution."`
}

type LookAtRealWorldParams

type LookAtRealWorldParams struct {
	RelativePan  float64 `json:"relative_pan,omitempty" description:"A value from -1.0 (right) to 1.0 (left) indicating how much to pan."`
	RelativeTilt float64 `json:"relative_tilt,omitempty" description:"A value from -1.0 (up) to 1.0 (down) indicating how much to tilt."`
	HighQuality  bool    `json:"high_quality,omitempty" description:"Use true if you want a high-resolution photo."`
}

type RunAppleScriptParams

type RunAppleScriptParams struct {
	ScriptLines []string `json:"script_lines" description:"One or more statements of valid AppleScript"`
}

type RunPowerShellCmdParams

type RunPowerShellCmdParams struct {
	Command string `json:"command"`
}

type RunPythonParams

type RunPythonParams struct {
	Statements []string `` /* 132-byte string literal not displayed */
}

type RunShellCmdParams

type RunShellCmdParams struct {
	Command string `json:"command"`
}

type SliceFileParams

type SliceFileParams struct {
	Path  string `json:"path" description:"The path to the file to read (don't use this on directories)."`
	Start int    `json:"start" description:"The start index of the slice to get. Can be negative to start from the end."`
	End   *int   `` /* 163-byte string literal not displayed */
}

type SpeakOutLoudParams

type SpeakOutLoudParams struct {
	Message string `json:"message"`
}

type SpliceFileParams

type SpliceFileParams struct {
	Path        string   `json:"path" description:"The path to the file to update."`
	Start       int      `json:"start" description:"The start index of the slice to delete and optionally replace."`
	DeleteCount int      `json:"deleteCount,omitempty" description:"The number of lines to delete from the slice."`
	InsertLines []string `json:"insertLines,omitempty" description:"The lines to insert at the start of the slice."`
}

type TakeScreenshotParams

type TakeScreenshotParams struct {
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL