Changeset 0.22.8 (#150)
This commit is contained in:
@@ -14,52 +14,52 @@ import (
|
||||
)
|
||||
|
||||
// ── Late Registration ────────────────────────
|
||||
// attachment_recall needs stores + objStore, which aren't available at
|
||||
// file_recall needs stores + objStore, which aren't available at
|
||||
// init time. Called from main.go after storage init.
|
||||
// If objStore is nil (storage not configured), the tool is not registered.
|
||||
|
||||
func RegisterAttachmentRecall(stores store.Stores, objStore storage.ObjectStore) {
|
||||
func RegisterFileRecall(stores store.Stores, objStore storage.ObjectStore) {
|
||||
if objStore == nil {
|
||||
log.Printf("ℹ attachment_recall: storage not configured, tool not registered")
|
||||
log.Printf("ℹ file_recall: storage not configured, tool not registered")
|
||||
return
|
||||
}
|
||||
Register(&attachmentRecallTool{
|
||||
Register(&fileRecallTool{
|
||||
stores: stores,
|
||||
objStore: objStore,
|
||||
})
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// attachment_recall
|
||||
// file_recall
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
const maxImageBytes = 10 * 1024 * 1024 // 10 MB cap for base64 images
|
||||
|
||||
type attachmentRecallTool struct {
|
||||
type fileRecallTool struct {
|
||||
stores store.Stores
|
||||
objStore storage.ObjectStore
|
||||
}
|
||||
|
||||
func (t *attachmentRecallTool) Definition() ToolDef {
|
||||
func (t *fileRecallTool) Definition() ToolDef {
|
||||
return ToolDef{
|
||||
Name: "attachment_recall",
|
||||
DisplayName: "Attachments",
|
||||
Name: "file_recall",
|
||||
DisplayName: "Files",
|
||||
Category: "context",
|
||||
Description: "Re-read file attachments from this conversation. " +
|
||||
Description: "Re-read files from this conversation. " +
|
||||
"Use action='list' to see available files, then action='read' " +
|
||||
"with an attachment_id to retrieve the file content. " +
|
||||
"with a file_id to retrieve the file content. " +
|
||||
"Documents return extracted text; images return base64 data.",
|
||||
Parameters: JSONSchema(map[string]interface{}{
|
||||
"action": PropEnum("Action: 'list' to see files, 'read' to get content", "list", "read"),
|
||||
"attachment_id": Prop("string", "Attachment ID to read (required for action='read')"),
|
||||
"file_id": Prop("string", "File ID to read (required for action='read')"),
|
||||
}, []string{"action"}),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *attachmentRecallTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
|
||||
func (t *fileRecallTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
|
||||
var args struct {
|
||||
Action string `json:"action"`
|
||||
AttachmentID string `json:"attachment_id"`
|
||||
FileID string `json:"file_id"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
|
||||
return "", fmt.Errorf("invalid arguments: %w", err)
|
||||
@@ -67,22 +67,22 @@ func (t *attachmentRecallTool) Execute(ctx context.Context, execCtx ExecutionCon
|
||||
|
||||
switch args.Action {
|
||||
case "list":
|
||||
return t.listAttachments(ctx, execCtx)
|
||||
return t.listFiles(ctx, execCtx)
|
||||
case "read":
|
||||
if args.AttachmentID == "" {
|
||||
return "", fmt.Errorf("attachment_id is required for action='read'")
|
||||
if args.FileID == "" {
|
||||
return "", fmt.Errorf("file_id is required for action='read'")
|
||||
}
|
||||
return t.readAttachment(ctx, execCtx, args.AttachmentID)
|
||||
return t.readFile(ctx, execCtx, args.FileID)
|
||||
default:
|
||||
return "", fmt.Errorf("invalid action: %q (use 'list' or 'read')", args.Action)
|
||||
}
|
||||
}
|
||||
|
||||
// listAttachments returns metadata for all attachments in the channel.
|
||||
func (t *attachmentRecallTool) listAttachments(ctx context.Context, execCtx ExecutionContext) (string, error) {
|
||||
atts, err := t.stores.Attachments.GetByChannel(ctx, execCtx.ChannelID)
|
||||
// listFiles returns metadata for all files in the channel.
|
||||
func (t *fileRecallTool) listFiles(ctx context.Context, execCtx ExecutionContext) (string, error) {
|
||||
atts, err := t.stores.Files.GetByChannel(ctx, execCtx.ChannelID, "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to list attachments: %w", err)
|
||||
return "", fmt.Errorf("failed to list files: %w", err)
|
||||
}
|
||||
|
||||
type attInfo struct {
|
||||
@@ -106,32 +106,32 @@ func (t *attachmentRecallTool) listAttachments(ctx context.Context, execCtx Exec
|
||||
})
|
||||
}
|
||||
|
||||
log.Printf("📎 attachment_recall: list → %d files in channel %s", len(items), execCtx.ChannelID)
|
||||
log.Printf("📎 file_recall: list → %d files in channel %s", len(items), execCtx.ChannelID)
|
||||
|
||||
out, _ := json.Marshal(map[string]interface{}{
|
||||
"attachments": items,
|
||||
"files": items,
|
||||
"count": len(items),
|
||||
"channel_id": execCtx.ChannelID,
|
||||
})
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// readAttachment returns the content of a specific attachment.
|
||||
func (t *attachmentRecallTool) readAttachment(ctx context.Context, execCtx ExecutionContext, attachmentID string) (string, error) {
|
||||
att, err := t.stores.Attachments.GetByID(ctx, attachmentID)
|
||||
// readFile returns the content of a specific file.
|
||||
func (t *fileRecallTool) readFile(ctx context.Context, execCtx ExecutionContext, fileID string) (string, error) {
|
||||
att, err := t.stores.Files.GetByID(ctx, fileID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("attachment not found: %s", attachmentID)
|
||||
return "", fmt.Errorf("file not found: %s", fileID)
|
||||
}
|
||||
|
||||
// Security: verify attachment belongs to this channel
|
||||
// Security: verify file belongs to this channel
|
||||
if att.ChannelID != execCtx.ChannelID {
|
||||
return "", fmt.Errorf("attachment %s does not belong to this conversation", attachmentID)
|
||||
return "", fmt.Errorf("file %s does not belong to this conversation", fileID)
|
||||
}
|
||||
|
||||
// Documents: return extracted text
|
||||
if !isImageType(att.ContentType) {
|
||||
if att.ExtractedText != nil && *att.ExtractedText != "" {
|
||||
log.Printf("📎 attachment_recall: read text → %s (%s, %d chars)",
|
||||
log.Printf("📎 file_recall: read text → %s (%s, %d chars)",
|
||||
att.Filename, att.ID, len(*att.ExtractedText))
|
||||
out, _ := json.Marshal(map[string]interface{}{
|
||||
"id": att.ID,
|
||||
@@ -153,20 +153,20 @@ func (t *attachmentRecallTool) readAttachment(ctx context.Context, execCtx Execu
|
||||
|
||||
reader, size, _, err := t.objStore.Get(ctx, att.StorageKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read attachment from storage: %w", err)
|
||||
return "", fmt.Errorf("failed to read file from storage: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// Read and base64-encode
|
||||
data := make([]byte, size)
|
||||
if _, err := io.ReadFull(reader, data); err != nil {
|
||||
return "", fmt.Errorf("failed to read attachment data: %w", err)
|
||||
return "", fmt.Errorf("failed to read file data: %w", err)
|
||||
}
|
||||
|
||||
b64 := base64.StdEncoding.EncodeToString(data)
|
||||
dataURI := fmt.Sprintf("data:%s;base64,%s", att.ContentType, b64)
|
||||
|
||||
log.Printf("📎 attachment_recall: read image → %s (%s, %d bytes)",
|
||||
log.Printf("📎 file_recall: read image → %s (%s, %d bytes)",
|
||||
att.Filename, att.ID, size)
|
||||
|
||||
out, _ := json.Marshal(map[string]interface{}{
|
||||
Reference in New Issue
Block a user