-
Notifications
You must be signed in to change notification settings - Fork 715
feat: support file upload in agent chat #2429
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
EItanya
merged 7 commits into
kagent-dev:release/v0.10.x
from
supreme-gg-gg:feat/file-parts
Aug 18, 2026
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
4d97802
ui and model converters
supreme-gg-gg 365e7ee
fix test
supreme-gg-gg 1b59763
extend openai responses file types
supreme-gg-gg ed44a1a
file parts test cleanup
supreme-gg-gg 2e28e44
review comments for ui
supreme-gg-gg b94cd59
review comments
supreme-gg-gg 1289c70
Merge branch 'release/v0.10.x' into feat/file-parts
supreme-gg-gg 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
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
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,245 @@ | ||
| package models | ||
|
|
||
| import ( | ||
| "encoding/base64" | ||
| "fmt" | ||
| "path" | ||
| "strings" | ||
|
|
||
| "google.golang.org/genai" | ||
| ) | ||
|
|
||
| // unsupportedFileNote is appended as text when a file part cannot be mapped | ||
| // to a provider-native input. Prefer this over silently dropping the part. | ||
| func unsupportedFileNote(name, mime string) string { | ||
| if name == "" { | ||
| name = "unnamed" | ||
| } | ||
| if mime == "" { | ||
| mime = "unknown" | ||
| } | ||
| return fmt.Sprintf("[unsupported file: %s (%s)]", name, mime) | ||
| } | ||
|
|
||
| func dataURI(mime string, data []byte) string { | ||
| return fmt.Sprintf("data:%s;base64,%s", mime, base64.StdEncoding.EncodeToString(data)) | ||
| } | ||
|
|
||
| func blobName(b *genai.Blob) string { | ||
| if b == nil { | ||
| return "" | ||
| } | ||
| return b.DisplayName | ||
| } | ||
|
|
||
| func fileDataName(f *genai.FileData) string { | ||
| if f == nil { | ||
| return "" | ||
| } | ||
| return f.DisplayName | ||
| } | ||
|
|
||
| func isImageMIME(mime string) bool { | ||
| return strings.HasPrefix(mime, "image/") | ||
| } | ||
|
|
||
| // OpenAI file support differs by API surface: | ||
| // | ||
| // - Chat Completions (`messages[].content[].type=file`, inline file_data): PDF only. | ||
| // - Responses (`input_file`): broad list (txt/md/csv/docx/xlsx/pptx/…). | ||
| func isOpenAIPDF(mime, name string) bool { | ||
| if strings.ToLower(mime) == "application/pdf" { | ||
| return true | ||
| } | ||
| return strings.ToLower(path.Ext(name)) == ".pdf" | ||
| } | ||
|
|
||
| // isOpenAIResponsesFileMIME is the Responses input_file allowlist (common types). | ||
| // Link: https://platform.openai.com/docs/guides/file-inputs | ||
| func isOpenAIResponsesFileMIME(mime, name string) bool { | ||
| if isOpenAIPDF(mime, name) { | ||
| return true | ||
| } | ||
| switch strings.ToLower(mime) { | ||
| case "text/plain", "text/markdown", "text/csv", "text/tsv", "text/html", "text/xml", "text/css", | ||
| "application/json", "application/xml", "application/rtf", "application/csv", "text/rtf", | ||
| "application/msword", | ||
| "application/vnd.openxmlformats-officedocument.wordprocessingml.document", | ||
| "application/vnd.ms-excel", | ||
| "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | ||
| "application/vnd.ms-powerpoint", | ||
| "application/vnd.openxmlformats-officedocument.presentationml.presentation", | ||
| "application/vnd.oasis.opendocument.text": | ||
| return true | ||
| } | ||
| if strings.HasPrefix(strings.ToLower(mime), "text/") { | ||
| return true | ||
| } | ||
| // Browsers often send "" / application/octet-stream — use extension. | ||
| switch strings.ToLower(path.Ext(name)) { | ||
| case ".pdf", ".txt", ".text", ".md", ".markdown", ".csv", ".tsv", ".html", ".htm", | ||
| ".xml", ".json", ".rtf", ".odt", | ||
| ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", | ||
| ".py", ".js", ".mjs", ".ts", ".go", ".java", ".c", ".cc", ".cpp", ".h", ".rb", | ||
| ".sh", ".yaml", ".yml", ".css", ".sql": | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func isAnthropicPDF(mime, name string) bool { | ||
| return isOpenAIPDF(mime, name) | ||
| } | ||
|
|
||
| // isTextFileMIME is true for UTF-8 text we can inline (CC fallback) or send as | ||
| // Anthropic PlainTextSource. Binary office formats are excluded. | ||
| func isTextFileMIME(mime, name string) bool { | ||
| switch strings.ToLower(mime) { | ||
| case "text/plain", "text/markdown", "text/csv", "text/tsv", "text/html", "text/xml", "text/css", | ||
| "application/json", "application/xml", "application/x-yaml", "text/yaml", "text/x-yaml": | ||
| return true | ||
| } | ||
| if strings.HasPrefix(strings.ToLower(mime), "text/") { | ||
| return true | ||
| } | ||
| switch strings.ToLower(path.Ext(name)) { | ||
| case ".txt", ".text", ".md", ".markdown", ".csv", ".tsv", ".html", ".htm", | ||
| ".xml", ".json", ".yaml", ".yml", ".css", | ||
| ".py", ".js", ".mjs", ".ts", ".go", ".java", ".c", ".cc", ".cpp", ".h", ".rb", ".sh", ".sql": | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func isAnthropicPlainText(mime, name string) bool { | ||
| return isTextFileMIME(mime, name) | ||
| } | ||
|
|
||
| // inlineFileText wraps file bytes as a labeled text chunk for APIs that cannot | ||
| // take the file natively (Chat Completions non-PDF). | ||
| func inlineFileText(name string, data []byte) string { | ||
| if name == "" { | ||
| name = "file" | ||
| } | ||
| return fmt.Sprintf("[file: %s]\n%s", name, string(data)) | ||
| } | ||
|
|
||
| // openAIFilename ensures OpenAI gets a filename (it uses the extension for type detection). | ||
| func openAIFilename(name, mime string) string { | ||
| if name != "" { | ||
| return name | ||
| } | ||
| switch strings.ToLower(mime) { | ||
| case "application/pdf": | ||
| return "document.pdf" | ||
| case "text/plain": | ||
| return "document.txt" | ||
| case "text/markdown": | ||
| return "document.md" | ||
| case "text/csv", "application/csv": | ||
| return "document.csv" | ||
| case "application/vnd.openxmlformats-officedocument.wordprocessingml.document": | ||
| return "document.docx" | ||
| case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": | ||
| return "document.xlsx" | ||
| case "application/vnd.openxmlformats-officedocument.presentationml.presentation": | ||
| return "document.pptx" | ||
| case "application/json": | ||
| return "document.json" | ||
| default: | ||
| return "document" | ||
| } | ||
| } | ||
|
|
||
| // bedrockImageFormat maps image MIME → Bedrock ImageFormat. Empty if unsupported. | ||
| func bedrockImageFormat(mime string) string { | ||
| switch strings.ToLower(mime) { | ||
| case "image/png": | ||
| return "png" | ||
| case "image/jpeg", "image/jpg": | ||
| return "jpeg" | ||
| case "image/gif": | ||
| return "gif" | ||
| case "image/webp": | ||
| return "webp" | ||
| default: | ||
| return "" | ||
| } | ||
| } | ||
|
|
||
| // bedrockDocumentFormat maps document MIME → Bedrock DocumentFormat. Empty if unsupported. | ||
| func bedrockDocumentFormat(mime, name string) string { | ||
| switch strings.ToLower(mime) { | ||
| case "application/pdf": | ||
| return "pdf" | ||
| case "text/csv": | ||
| return "csv" | ||
| case "application/msword": | ||
| return "doc" | ||
| case "application/vnd.openxmlformats-officedocument.wordprocessingml.document": | ||
| return "docx" | ||
| case "application/vnd.ms-excel": | ||
| return "xls" | ||
| case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": | ||
| return "xlsx" | ||
| case "text/html": | ||
| return "html" | ||
| case "text/plain": | ||
| return "txt" | ||
| case "text/markdown": | ||
| return "md" | ||
| } | ||
| // Fallback: extension from display name. | ||
| switch strings.ToLower(path.Ext(name)) { | ||
| case ".pdf": | ||
| return "pdf" | ||
| case ".csv": | ||
| return "csv" | ||
| case ".doc": | ||
| return "doc" | ||
| case ".docx": | ||
| return "docx" | ||
| case ".xls": | ||
| return "xls" | ||
| case ".xlsx": | ||
| return "xlsx" | ||
| case ".html", ".htm": | ||
| return "html" | ||
| case ".txt": | ||
| return "txt" | ||
| case ".md", ".markdown": | ||
| return "md" | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| // bedrockSafeDocName keeps only chars Bedrock accepts in DocumentBlock.Name | ||
| // (alphanumeric, single spaces, -()[]; max 200). Neutral renaming is left to callers. | ||
| func bedrockSafeDocName(name string) string { | ||
| if name == "" { | ||
| return "document" | ||
| } | ||
| var b strings.Builder | ||
| prevSpace := false | ||
| for _, r := range name { | ||
| switch { | ||
| case (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || | ||
| r == '-' || r == '(' || r == ')' || r == '[' || r == ']': | ||
| b.WriteRune(r) | ||
| prevSpace = false | ||
| case r == ' ' || r == '_' || r == '.': | ||
| if !prevSpace && b.Len() > 0 { | ||
| b.WriteByte(' ') | ||
| prevSpace = true | ||
| } | ||
| } | ||
| } | ||
| out := strings.TrimSpace(b.String()) | ||
| if out == "" { | ||
| return "document" | ||
| } | ||
| if rs := []rune(out); len(rs) > 200 { | ||
| return string(rs[:200]) | ||
| } | ||
| return out | ||
| } | ||
|
iplay88keys marked this conversation as resolved.
|
||
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.
Uh oh!
There was an error while loading. Please reload this page.