From 1be146980cf941128210b89f77ace15a1d2e6344 Mon Sep 17 00:00:00 2001 From: Bruno Michel Date: Thu, 3 Oct 2024 09:21:08 +0200 Subject: [PATCH] Add an endpoint to fill the description of a file with AI --- docs/files.md | 77 ++++++++++++++++++++++++++++++++++++++++++++-- model/rag/file.go | 72 +++++++++++++++++++++++++++++++++++++++++++ web/files/files.go | 20 ++++++++++++ 3 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 model/rag/file.go diff --git a/docs/files.md b/docs/files.md index 6b4c579eaf0..ca045f4953e 100644 --- a/docs/files.md +++ b/docs/files.md @@ -958,8 +958,8 @@ Get an image that shows the first page of a PDF (at most 1080x1920). Get a thumbnail of a file (for an image & pdf only). `:format` can be `tiny` (96x96) `small` (640x480), `medium` (1280x720), or `large` (1920x1080). -This API does not require authentication because the secret acts as a token. -This secret is valid for 10 minutes, after which the link will return an error. +This API does not require authentication because the secret acts as a token. +This secret is valid for 10 minutes, after which the link will return an error. To retrieve a new functional link, you must query the files API again to obtain a new secret. @@ -1400,6 +1400,79 @@ Content-Type: application/vnd.api+json The same status codes can be encountered as the `PATCH /files/:file-id` route. +### POST /files/:id/description + +This endpoint fills the `metadata.description` field of a file with a +description generated by the IA from the content of this file. + +#### Request + +```http +POST /files/9152d568-7e7c-11e6-a377-37cbfb190b4b/description HTTP/1.1 +``` + +#### Response + +```http +HTTP/1.1 200 OK +Content-Type: application/json.vnd+api +``` + +```json +{ + "data": { + "type": "io.cozy.files", + "id": "9152d568-7e7c-11e6-a377-37cbfb190b4b", + "meta": { + "rev": "2-20900ae0" + }, + "attributes": { + "type": "file", + "name": "hi.txt", + "trashed": false, + "md5sum": "ODZmYjI2OWQxOTBkMmM4NQo=", + "created_at": "2016-09-19T12:38:04Z", + "updated_at": "2016-09-19T12:38:04Z", + "tags": ["poem"], + "size": 12, + "executable": false, + "class": "document", + "mime": "text/plain", + "metadata": { + "description": "Explores love's complexities through vivid imagery and heartfelt emotions" + }, + "cozyMetadata": { + "doctypeVersion": "1", + "metadataVersion": 1, + "createdAt": "2016-09-20T18:32:49Z", + "createdByApp": "drive", + "createdOn": "https://cozy.example.com/", + "updatedAt": "2016-09-22T13:32:51Z", + "uploadedAt": "2016-09-21T04:27:50Z", + "uploadedOn": "https://cozy.example.com/", + "uploadedBy": { + "slug": "drive" + } + } + }, + "relationships": { + "parent": { + "links": { + "related": "/files/f2f36fec-8018-11e6-abd8-8b3814d9a465" + }, + "data": { + "type": "io.cozy.files", + "id": "f2f36fec-8018-11e6-abd8-8b3814d9a465" + } + } + }, + "links": { + "self": "/files/9152d568-7e7c-11e6-a377-37cbfb190b4b" + } + } +} +``` + ### POST /files/archive Create an archive. The body of the request lists the files and directories that diff --git a/model/rag/file.go b/model/rag/file.go new file mode 100644 index 00000000000..11cf81ae134 --- /dev/null +++ b/model/rag/file.go @@ -0,0 +1,72 @@ +package rag + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/cozy/cozy-stack/model/instance" + "github.com/cozy/cozy-stack/model/vfs" + "github.com/cozy/cozy-stack/pkg/couchdb" + "github.com/labstack/echo/v4" +) + +func AddDescriptionToFile(inst *instance.Instance, file *vfs.FileDoc) (*vfs.FileDoc, error) { + content, err := inst.VFS().OpenFile(file) + if err != nil { + return nil, err + } + defer content.Close() + description, err := callRAGDescription(inst, content, file.Mime) + if err != nil { + return nil, err + } + newfile := file.Clone().(*vfs.FileDoc) + if newfile.Metadata == nil { + newfile.Metadata = make(map[string]interface{}) + } + newfile.Metadata["description"] = description + if err := couchdb.UpdateDocWithOld(inst, newfile, file); err != nil { + return nil, err + } + return newfile, nil +} + +func callRAGDescription(inst *instance.Instance, content io.Reader, mime string) (string, error) { + ragServer := inst.RAGServer() + if ragServer.URL == "" { + return "", errors.New("no RAG server configured") + } + u, err := url.Parse(ragServer.URL) + if err != nil { + return "", err + } + u.Path = "/description" + req, err := http.NewRequest(http.MethodPost, u.String(), content) + if err != nil { + return "", err + } + req.Header.Add(echo.HeaderAuthorization, "Bearer "+ragServer.APIKey) + req.Header.Add("Content-Type", mime) + res, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer res.Body.Close() + if res.StatusCode >= 300 { + return "", fmt.Errorf("POST status code: %d", res.StatusCode) + } + var data struct { + Description string `json:"description"` + } + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return "", err + } + if data.Description == "" { + return "", errors.New("no description") + } + return data.Description, nil +} diff --git a/web/files/files.go b/web/files/files.go index 71047ba90ba..7743c23a36a 100644 --- a/web/files/files.go +++ b/web/files/files.go @@ -25,6 +25,7 @@ import ( "github.com/cozy/cozy-stack/model/note" "github.com/cozy/cozy-stack/model/oauth" "github.com/cozy/cozy-stack/model/permission" + "github.com/cozy/cozy-stack/model/rag" "github.com/cozy/cozy-stack/model/sharing" "github.com/cozy/cozy-stack/model/vfs" "github.com/cozy/cozy-stack/pkg/assets/statik" @@ -385,6 +386,24 @@ func FileCopyHandler(c echo.Context) error { return FileData(c, http.StatusCreated, newdoc, false, nil) } +func AddDescription(c echo.Context) error { + inst := middlewares.GetInstance(c) + fileID := c.Param("file-id") + doc, err := inst.VFS().FileByID(fileID) + if err != nil { + return WrapVfsError(err) + } + err = checkPerm(c, permission.POST, nil, doc) + if err != nil { + return err + } + newdoc, err := rag.AddDescriptionToFile(inst, doc) + if err != nil { + return WrapVfsError(err) + } + return FileData(c, http.StatusOK, newdoc, false, nil) +} + // ModifyMetadataByIDHandler handles PATCH requests on /files/:file-id // // It can be used to modify the file or directory metadata, as well as @@ -1932,6 +1951,7 @@ func Routes(router *echo.Group) { router.PATCH("/metadata", ModifyMetadataByPathHandler) router.PATCH("/:file-id", ModifyMetadataByIDHandler) router.PATCH("/", ModifyMetadataByIDInBatchHandler) + router.POST("/:file-id/description", AddDescription) router.POST("/shared-drives", SharedDrivesCreationHandler) router.POST("/", CreationHandler)