Skip to content

Commit be2719b

Browse files
committed
added basic support for text files
1 parent 746f43f commit be2719b

File tree

15 files changed

+408
-47
lines changed

15 files changed

+408
-47
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package app
2+
3+
import (
4+
"context"
5+
"io"
6+
"net/http"
7+
"runtime"
8+
"sync"
9+
10+
"github.com/0xdeafcafe/bloefish/services/airelay"
11+
"github.com/0xdeafcafe/bloefish/services/fileupload"
12+
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
13+
"golang.org/x/sync/errgroup"
14+
)
15+
16+
type downloadedFile struct {
17+
fileupload.File
18+
Content []byte
19+
}
20+
21+
func (a *App) downloadFiles(ctx context.Context, owner *airelay.Actor, fileIDs []string) (map[string]*downloadedFile, error) {
22+
if len(fileIDs) == 0 {
23+
return map[string]*downloadedFile{}, nil
24+
}
25+
26+
files, err := a.FileUploadService.GetManyFiles(ctx, &fileupload.GetManyFilesRequest{
27+
FileIDs: fileIDs,
28+
Owner: &fileupload.Actor{
29+
Type: fileupload.ActorType(owner.Type),
30+
Identifier: owner.Identifier,
31+
},
32+
AllowDeleted: false,
33+
IncludeAccessURL: true,
34+
})
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
egGroup, egCtx := errgroup.WithContext(ctx)
40+
egGroup.SetLimit(runtime.NumCPU() * 4)
41+
42+
client := http.Client{
43+
Transport: otelhttp.NewTransport(http.DefaultTransport),
44+
}
45+
downloadedFiles := make(map[string]*downloadedFile, len(files.Files))
46+
mu := sync.Mutex{}
47+
48+
for _, file := range files.Files {
49+
egGroup.Go(func() error {
50+
req, err := http.NewRequestWithContext(egCtx, http.MethodGet, *file.PresignedAccessURL, nil)
51+
if err != nil {
52+
return err
53+
}
54+
55+
resp, err := client.Do(req)
56+
if err != nil {
57+
return err
58+
}
59+
defer resp.Body.Close()
60+
61+
content, err := io.ReadAll(resp.Body)
62+
if err != nil {
63+
return err
64+
}
65+
66+
mu.Lock()
67+
defer mu.Unlock()
68+
downloadedFiles[file.ID] = &downloadedFile{
69+
File: *file,
70+
Content: content,
71+
}
72+
73+
return nil
74+
})
75+
}
76+
77+
return nil, nil
78+
}

services/airelay/internal/app/invoke_streaming_conversation_message.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,41 @@ import (
1515
)
1616

1717
func (a *App) InvokeStreamingConversationMessage(ctx context.Context, req *airelay.InvokeStreamingConversationMessageRequest) (*airelay.InvokeStreamingConversationMessageResponse, error) {
18+
fileIDs := make([]string, 0, len(req.Messages))
19+
for _, msg := range req.Messages {
20+
fileIDs = append(fileIDs, msg.FileIDs...)
21+
}
22+
23+
downloadedFiles, err := a.downloadFiles(ctx, req.Owner, fileIDs)
24+
if err != nil {
25+
return nil, err
26+
}
27+
1828
messages := make([]relay.Message, len(req.Messages))
1929
for i, msg := range req.Messages {
30+
fileContent := ""
31+
32+
if len(msg.FileIDs) > 0 {
33+
file := downloadedFiles[msg.FileIDs[0]]
34+
if file == nil {
35+
return nil, cher.New("file_not_found", cher.M{
36+
"file_id": msg.FileIDs[0],
37+
})
38+
}
39+
40+
fileContent = fmt.Sprintf("\n\nFile name: %s\nFile content:\n%s", file.Name, string(file.Content))
41+
}
42+
2043
switch msg.Owner.Type {
2144
case airelay.ActorTypeBot:
2245
messages[i] = relay.Message{
2346
Role: relay.RoleAssistant,
24-
Content: msg.Content,
47+
Content: msg.Content + fileContent,
2548
}
2649
case airelay.ActorTypeUser:
2750
messages[i] = relay.Message{
2851
Role: relay.RoleUser,
29-
Content: msg.Content,
52+
Content: msg.Content + fileContent,
3053
}
3154
}
3255
}

services/conversation/internal/app/create_conversation_message_reply.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func (a *App) createConversationMessageReply(
4444
Type: airelay.ActorType(skillSet.Owner.Type),
4545
Identifier: skillSet.Owner.Identifier,
4646
},
47-
FileIDs: []string{}, // Skill sets current can't have files
47+
FileIDs: []string{}, // Skill sets currently can't have files
4848
Content: fmt.Sprintf(
4949
"Use the following instructions to guide your responses or to learn more context about the subject: %s",
5050
skillSet.Prompt,
@@ -63,7 +63,7 @@ func (a *App) createConversationMessageReply(
6363
Type: airelay.ActorType(interaction.Owner.Type),
6464
Identifier: interaction.Owner.Identifier,
6565
},
66-
FileIDs: []string{}, // TODO(afr): Handle files
66+
FileIDs: interaction.FileIDs,
6767
Content: interaction.MessageContent,
6868
})
6969
}

services/conversation/internal/app/generate_conversation_title.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func (a *App) generateConversationTitle(
4343
cmd.Interaction.MessageContent,
4444
),
4545
Owner: cmd.Owner,
46-
FileIDs: []string{},
46+
FileIDs: []string{}, // Titles should not support files
4747
}}
4848

4949
if cmd.UseStreaming {

services/fileupload/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,49 @@ interface Response {
8585
}
8686
```
8787

88+
#### `get_many_files`
89+
90+
Gets many files by ID.
91+
92+
If `include_access_url` is set to `true`, then the `presigned_access_url` will be included in the response.
93+
94+
If `access_url_expiry_seconds` is set to a number, then the `presigned_access_url` will expire after that many seconds. If it is set to `null`, then the URL will have a default expiry time of 15 minutes.
95+
96+
If `allow_deleted` is `true` then deleted files will be included. If `false`, then if a deleted file is requested an error will be returned.
97+
98+
If `owner` is provided it will filter the results to only include file owned by the specified user.
99+
100+
**Contract**
101+
102+
```typescript
103+
interface Request {
104+
file_ids: string[];
105+
owner: {
106+
type: 'user';
107+
identifier: string;
108+
} | null;
109+
110+
allow_deleted: boolean | null;
111+
include_access_url: boolean;
112+
access_url_expiry_seconds: number | null;
113+
}
114+
115+
interface Response {
116+
files: {
117+
id: string;
118+
name: string;
119+
size: number;
120+
mime_type: string;
121+
owner: {
122+
type: 'user';
123+
identifier: string;
124+
};
125+
126+
presigned_access_url: string | null;
127+
}[];
128+
}
129+
```
130+
88131

89132
## Uploading to an upload url
90133

services/fileupload/fileupload.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type Service interface {
88
CreateUpload(ctx context.Context, req *CreateUploadRequest) (*CreateUploadResponse, error)
99
ConfirmUpload(ctx context.Context, req *ConfirmUploadRequest) error
1010
GetFile(ctx context.Context, req *GetFileRequest) (*GetFileResponse, error)
11+
GetManyFiles(ctx context.Context, req *GetManyFilesRequest) (*GetManyFilesResponse, error)
1112
}
1213

1314
type ActorType string
@@ -21,6 +22,15 @@ type Actor struct {
2122
Identifier string `json:"identifier"`
2223
}
2324

25+
type File struct {
26+
ID string `json:"id"`
27+
Name string `json:"name"`
28+
Size int64 `json:"size"`
29+
MIMEType string `json:"mime_type"`
30+
Owner *Actor `json:"owner"`
31+
PresignedAccessURL *string `json:"presigned_access_url"`
32+
}
33+
2434
type CreateUploadRequest struct {
2535
Name string `json:"name"`
2636
Size int64 `json:"size"`
@@ -44,10 +54,17 @@ type GetFileRequest struct {
4454
}
4555

4656
type GetFileResponse struct {
47-
ID string `json:"id"`
48-
Name string `json:"name"`
49-
Size int64 `json:"size"`
50-
MIMEType string `json:"mime_type"`
51-
Owner *Actor `json:"owner"`
52-
PresignedAccessURL *string `json:"presigned_access_url"`
57+
File
58+
}
59+
60+
type GetManyFilesRequest struct {
61+
FileIDs []string `json:"file_ids"`
62+
Owner *Actor `json:"owner"`
63+
AllowDeleted bool `json:"allow_deleted"`
64+
IncludeAccessURL bool `json:"include_access_url"`
65+
AccessURLExpirySeconds *int `json:"access_url_expiry_seconds"`
66+
}
67+
68+
type GetManyFilesResponse struct {
69+
Files []*File `json:"files"`
5370
}

services/fileupload/internal/app/app.go

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package app
22

33
import (
44
"context"
5-
"time"
65

76
"github.com/0xdeafcafe/bloefish/libraries/cher"
87
"github.com/0xdeafcafe/bloefish/services/fileupload"
@@ -64,38 +63,3 @@ func (a *App) ConfirmUpload(ctx context.Context, req *fileupload.ConfirmUploadRe
6463

6564
return nil
6665
}
67-
68-
func (a *App) GetFile(ctx context.Context, req *fileupload.GetFileRequest) (*fileupload.GetFileResponse, error) {
69-
file, err := a.FileRepository.Get(ctx, req.FileID)
70-
if err != nil {
71-
return nil, err
72-
}
73-
74-
var presignedAccessURL *string
75-
if req.IncludeAccessURL {
76-
expiry := time.Minute * 15
77-
if req.AccessURLExpirySeconds != nil {
78-
expirySeconds := *req.AccessURLExpirySeconds
79-
expiry = time.Second * time.Duration(expirySeconds)
80-
}
81-
82-
presignedURL, err := a.FileObjectService.CreatePresignedDownloadURL(ctx, file.ID, expiry)
83-
if err != nil {
84-
return nil, err
85-
}
86-
87-
presignedAccessURL = &presignedURL
88-
}
89-
90-
return &fileupload.GetFileResponse{
91-
ID: file.ID,
92-
Name: file.Name,
93-
Size: file.Size,
94-
MIMEType: file.MIMEType,
95-
Owner: &fileupload.Actor{
96-
Type: fileupload.ActorType(file.Owner.Type),
97-
Identifier: file.Owner.Identifier,
98-
},
99-
PresignedAccessURL: presignedAccessURL,
100-
}, nil
101-
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package app
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/0xdeafcafe/bloefish/services/fileupload"
8+
)
9+
10+
func (a *App) GetFile(ctx context.Context, req *fileupload.GetFileRequest) (*fileupload.GetFileResponse, error) {
11+
file, err := a.FileRepository.Get(ctx, req.FileID)
12+
if err != nil {
13+
return nil, err
14+
}
15+
16+
var presignedAccessURL *string
17+
if req.IncludeAccessURL {
18+
expiry := time.Minute * 15
19+
if req.AccessURLExpirySeconds != nil {
20+
expirySeconds := *req.AccessURLExpirySeconds
21+
expiry = time.Second * time.Duration(expirySeconds)
22+
}
23+
24+
presignedURL, err := a.FileObjectService.CreatePresignedDownloadURL(ctx, file.ID, expiry)
25+
if err != nil {
26+
return nil, err
27+
}
28+
29+
presignedAccessURL = &presignedURL
30+
}
31+
32+
return &fileupload.GetFileResponse{
33+
File: fileupload.File{
34+
ID: file.ID,
35+
Name: file.Name,
36+
Size: file.Size,
37+
MIMEType: file.MIMEType,
38+
Owner: &fileupload.Actor{
39+
Type: fileupload.ActorType(file.Owner.Type),
40+
Identifier: file.Owner.Identifier,
41+
},
42+
PresignedAccessURL: presignedAccessURL,
43+
},
44+
}, nil
45+
}

0 commit comments

Comments
 (0)