-
Notifications
You must be signed in to change notification settings - Fork 531
Create Interface for Backend AI Providers #2572
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
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
6bca52a
create new usechat backend interface
sawka c2c5002
refactor all usechat code to use the new backend interface
sawka 6a378b2
remove some unused fields, small refactoring
sawka 69616a0
Merge remote-tracking branch 'origin/main' into sawka/usechat-interface
sawka f1acc84
extract some generic utility functions from openai-convertmessage
sawka File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| // Copyright 2025, Command Line Inc. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package aiutil | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "crypto/sha256" | ||
| "encoding/base64" | ||
| "encoding/hex" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strconv" | ||
| "strings" | ||
|
|
||
| "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes" | ||
| "github.com/wavetermdev/waveterm/pkg/util/utilfn" | ||
| ) | ||
|
|
||
| // ExtractXmlAttribute extracts an attribute value from an XML-like tag. | ||
| // Expects double-quoted strings where internal quotes are encoded as ". | ||
| // Returns the unquoted value and true if found, or empty string and false if not found or invalid. | ||
| func ExtractXmlAttribute(tag, attrName string) (string, bool) { | ||
| attrStart := strings.Index(tag, attrName+"=") | ||
| if attrStart == -1 { | ||
| return "", false | ||
| } | ||
|
|
||
| pos := attrStart + len(attrName+"=") | ||
| start := strings.Index(tag[pos:], `"`) | ||
| if start == -1 { | ||
| return "", false | ||
| } | ||
| start += pos | ||
|
|
||
| end := strings.Index(tag[start+1:], `"`) | ||
| if end == -1 { | ||
| return "", false | ||
| } | ||
| end += start + 1 | ||
|
|
||
| quotedValue := tag[start : end+1] | ||
| value, err := strconv.Unquote(quotedValue) | ||
| if err != nil { | ||
| return "", false | ||
| } | ||
|
|
||
| value = strings.ReplaceAll(value, """, `"`) | ||
| return value, true | ||
| } | ||
|
|
||
| // GenerateDeterministicSuffix creates an 8-character hash from input strings | ||
| func GenerateDeterministicSuffix(inputs ...string) string { | ||
| hasher := sha256.New() | ||
| for _, input := range inputs { | ||
| hasher.Write([]byte(input)) | ||
| } | ||
| hash := hasher.Sum(nil) | ||
| return hex.EncodeToString(hash)[:8] | ||
| } | ||
|
|
||
| // ExtractImageUrl extracts an image URL from either URL field (http/https/data) or raw Data | ||
| func ExtractImageUrl(data []byte, url, mimeType string) (string, error) { | ||
| if url != "" { | ||
| if !strings.HasPrefix(url, "data:") && | ||
| !strings.HasPrefix(url, "http://") && | ||
| !strings.HasPrefix(url, "https://") { | ||
| return "", fmt.Errorf("unsupported URL protocol in file part: %s", url) | ||
| } | ||
| return url, nil | ||
| } | ||
| if len(data) > 0 { | ||
| base64Data := base64.StdEncoding.EncodeToString(data) | ||
| return fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data), nil | ||
| } | ||
| return "", fmt.Errorf("file part missing both url and data") | ||
| } | ||
|
|
||
| // ExtractTextData extracts text data from either Data field or URL field (data: URLs only) | ||
| func ExtractTextData(data []byte, url string) ([]byte, error) { | ||
| if len(data) > 0 { | ||
| return data, nil | ||
| } | ||
| if url != "" { | ||
| if strings.HasPrefix(url, "data:") { | ||
| _, decodedData, err := utilfn.DecodeDataURL(url) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to decode data URL for text/plain file: %w", err) | ||
| } | ||
| return decodedData, nil | ||
| } | ||
| return nil, fmt.Errorf("dropping text/plain file with URL (must be fetched and converted to data)") | ||
| } | ||
| return nil, fmt.Errorf("text/plain file part missing data") | ||
| } | ||
|
|
||
| // FormatAttachedTextFile formats a text file attachment with proper encoding and deterministic suffix | ||
| func FormatAttachedTextFile(fileName string, textContent []byte) string { | ||
| if fileName == "" { | ||
| fileName = "untitled.txt" | ||
| } | ||
|
|
||
| encodedFileName := strings.ReplaceAll(fileName, `"`, """) | ||
| quotedFileName := strconv.Quote(encodedFileName) | ||
|
|
||
| textStr := string(textContent) | ||
| deterministicSuffix := GenerateDeterministicSuffix(textStr, fileName) | ||
| return fmt.Sprintf("<AttachedTextFile_%s file_name=%s>\n%s\n</AttachedTextFile_%s>", deterministicSuffix, quotedFileName, textStr, deterministicSuffix) | ||
| } | ||
|
|
||
| // FormatAttachedDirectoryListing formats a directory listing attachment with proper encoding and deterministic suffix | ||
| func FormatAttachedDirectoryListing(directoryName, jsonContent string) string { | ||
| if directoryName == "" { | ||
| directoryName = "unnamed-directory" | ||
| } | ||
|
|
||
| encodedDirName := strings.ReplaceAll(directoryName, `"`, """) | ||
| quotedDirName := strconv.Quote(encodedDirName) | ||
|
|
||
| deterministicSuffix := GenerateDeterministicSuffix(jsonContent, directoryName) | ||
| return fmt.Sprintf("<AttachedDirectoryListing_%s directory_name=%s>\n%s\n</AttachedDirectoryListing_%s>", deterministicSuffix, quotedDirName, jsonContent, deterministicSuffix) | ||
| } | ||
|
|
||
| // ConvertDataUserFile converts OpenAI attached file/directory blocks to UIMessagePart | ||
| // Returns (found, part) where found indicates if the prefix was matched, | ||
| // and part is the converted UIMessagePart (can be nil if parsing failed) | ||
| func ConvertDataUserFile(blockText string) (bool, *uctypes.UIMessagePart) { | ||
| if strings.HasPrefix(blockText, "<AttachedTextFile_") { | ||
| openTagEnd := strings.Index(blockText, "\n") | ||
| if openTagEnd == -1 || blockText[openTagEnd-1] != '>' { | ||
| return true, nil | ||
| } | ||
|
|
||
| openTag := blockText[:openTagEnd] | ||
| fileName, ok := ExtractXmlAttribute(openTag, "file_name") | ||
| if !ok { | ||
| return true, nil | ||
| } | ||
|
|
||
| return true, &uctypes.UIMessagePart{ | ||
| Type: "data-userfile", | ||
| Data: uctypes.UIMessageDataUserFile{ | ||
| FileName: fileName, | ||
| MimeType: "text/plain", | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| if strings.HasPrefix(blockText, "<AttachedDirectoryListing_") { | ||
| openTagEnd := strings.Index(blockText, "\n") | ||
| if openTagEnd == -1 || blockText[openTagEnd-1] != '>' { | ||
| return true, nil | ||
| } | ||
|
|
||
| openTag := blockText[:openTagEnd] | ||
| directoryName, ok := ExtractXmlAttribute(openTag, "directory_name") | ||
| if !ok { | ||
| return true, nil | ||
| } | ||
|
|
||
| return true, &uctypes.UIMessagePart{ | ||
| Type: "data-userfile", | ||
| Data: uctypes.UIMessageDataUserFile{ | ||
| FileName: directoryName, | ||
| MimeType: "directory", | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| return false, nil | ||
| } | ||
|
|
||
| func JsonEncodeRequestBody(reqBody any) (bytes.Buffer, error) { | ||
| var buf bytes.Buffer | ||
| encoder := json.NewEncoder(&buf) | ||
| encoder.SetEscapeHTML(false) | ||
| err := encoder.Encode(reqBody) | ||
| if err != nil { | ||
| return buf, err | ||
| } | ||
| return buf, nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential issues with attribute name matching.
Lines 24 and 29: The current implementation searches for
attrName+"=", which could match partial attribute names. For example, searching for"name="would also match"filename=".Additionally, the parser expects no whitespace around the
=sign. If the XML allowsattrName = "value"(with spaces), this would fail.Consider improving the attribute matching:
Alternatively, verify that the XML format strictly disallows spaces around
=and that attribute names don't overlap.🤖 Prompt for AI Agents