Bake in thread directives
This commit is contained in:
@@ -16,16 +16,19 @@ import (
|
||||
"time"
|
||||
|
||||
"codex-telegram-bot/internal/codexapp"
|
||||
"codex-telegram-bot/internal/codexstate"
|
||||
"codex-telegram-bot/internal/store"
|
||||
)
|
||||
|
||||
const (
|
||||
telegramDownloadLimit = 20 * 1024 * 1024
|
||||
resumeThreadPageSize = 8
|
||||
commandSummaryLimit = 120
|
||||
telegramPhotoDirectiveStart = "<!-- telegram-photo "
|
||||
telegramPhotoDirectiveEnd = " -->"
|
||||
telegramPhotoCaptionLimit = 1024
|
||||
telegramDownloadLimit = 20 * 1024 * 1024
|
||||
resumeThreadPageSize = 8
|
||||
commandSummaryLimit = 120
|
||||
telegramPhotoDirectiveStart = "<!-- telegram-photo "
|
||||
telegramThreadRenameDirectiveStart = "<!-- codex-thread-rename "
|
||||
telegramThreadCWDDirectiveStart = "<!-- codex-thread-cwd "
|
||||
telegramDirectiveEnd = " -->"
|
||||
telegramPhotoCaptionLimit = 1024
|
||||
)
|
||||
|
||||
type Bot struct {
|
||||
@@ -35,6 +38,8 @@ type Bot struct {
|
||||
logger *log.Logger
|
||||
|
||||
uploadDir string
|
||||
codexHome string
|
||||
codexStateDB string
|
||||
defaultModel string
|
||||
defaultSandbox string
|
||||
pollTimeout time.Duration
|
||||
@@ -45,8 +50,10 @@ type Bot struct {
|
||||
}
|
||||
|
||||
type assistantMessageSegment struct {
|
||||
Text string
|
||||
Photo *assistantPhotoDirective
|
||||
Text string
|
||||
Photo *assistantPhotoDirective
|
||||
ThreadRename *assistantThreadRenameDirective
|
||||
ThreadCWD *assistantThreadCWDDirective
|
||||
}
|
||||
|
||||
type assistantPhotoDirective struct {
|
||||
@@ -54,6 +61,14 @@ type assistantPhotoDirective struct {
|
||||
Caption string `json:"caption,omitempty"`
|
||||
}
|
||||
|
||||
type assistantThreadRenameDirective struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
type assistantThreadCWDDirective struct {
|
||||
CWD string `json:"cwd"`
|
||||
}
|
||||
|
||||
type outputState struct {
|
||||
chatID int64
|
||||
assistant strings.Builder
|
||||
@@ -95,7 +110,7 @@ type codexThreadItemView struct {
|
||||
} `json:"changes"`
|
||||
}
|
||||
|
||||
func NewBot(tg *Client, st *store.Store, codex *codexapp.Client, uploadDir, defaultModel, defaultSandbox string, pollTimeout time.Duration, logger *log.Logger) *Bot {
|
||||
func NewBot(tg *Client, st *store.Store, codex *codexapp.Client, uploadDir, codexHome, codexStateDB, defaultModel, defaultSandbox string, pollTimeout time.Duration, logger *log.Logger) *Bot {
|
||||
if logger == nil {
|
||||
logger = log.Default()
|
||||
}
|
||||
@@ -105,6 +120,8 @@ func NewBot(tg *Client, st *store.Store, codex *codexapp.Client, uploadDir, defa
|
||||
codex: codex,
|
||||
logger: logger,
|
||||
uploadDir: uploadDir,
|
||||
codexHome: codexHome,
|
||||
codexStateDB: codexStateDB,
|
||||
defaultModel: defaultModel,
|
||||
defaultSandbox: defaultSandbox,
|
||||
pollTimeout: pollTimeout,
|
||||
@@ -1857,18 +1874,58 @@ func splitAssistantMessageSegments(text string) []assistantMessageSegment {
|
||||
segments = append(segments, assistantMessageSegment{Photo: &directive})
|
||||
continue
|
||||
}
|
||||
if directive, ok := parseAssistantThreadRenameDirectiveLine(body); ok {
|
||||
flushVisible()
|
||||
segments = append(segments, assistantMessageSegment{ThreadRename: &directive})
|
||||
continue
|
||||
}
|
||||
if directive, ok := parseAssistantThreadCWDDirectiveLine(body); ok {
|
||||
flushVisible()
|
||||
segments = append(segments, assistantMessageSegment{ThreadCWD: &directive})
|
||||
continue
|
||||
}
|
||||
visible.WriteString(line)
|
||||
}
|
||||
flushVisible()
|
||||
return segments
|
||||
}
|
||||
|
||||
func parseAssistantThreadRenameDirectiveLine(line string) (assistantThreadRenameDirective, bool) {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, telegramThreadRenameDirectiveStart) || !strings.HasSuffix(trimmed, telegramDirectiveEnd) {
|
||||
return assistantThreadRenameDirective{}, false
|
||||
}
|
||||
raw := strings.TrimSuffix(strings.TrimPrefix(trimmed, telegramThreadRenameDirectiveStart), telegramDirectiveEnd)
|
||||
raw = strings.TrimSpace(raw)
|
||||
var directive assistantThreadRenameDirective
|
||||
if err := json.Unmarshal([]byte(raw), &directive); err != nil {
|
||||
return assistantThreadRenameDirective{}, false
|
||||
}
|
||||
directive.Title = normalizeThreadTitle(directive.Title)
|
||||
return directive, true
|
||||
}
|
||||
|
||||
func parseAssistantThreadCWDDirectiveLine(line string) (assistantThreadCWDDirective, bool) {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, telegramThreadCWDDirectiveStart) || !strings.HasSuffix(trimmed, telegramDirectiveEnd) {
|
||||
return assistantThreadCWDDirective{}, false
|
||||
}
|
||||
raw := strings.TrimSuffix(strings.TrimPrefix(trimmed, telegramThreadCWDDirectiveStart), telegramDirectiveEnd)
|
||||
raw = strings.TrimSpace(raw)
|
||||
var directive assistantThreadCWDDirective
|
||||
if err := json.Unmarshal([]byte(raw), &directive); err != nil {
|
||||
return assistantThreadCWDDirective{}, false
|
||||
}
|
||||
directive.CWD = strings.TrimSpace(directive.CWD)
|
||||
return directive, true
|
||||
}
|
||||
|
||||
func parseAssistantPhotoDirectiveLine(line string) (assistantPhotoDirective, bool) {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, telegramPhotoDirectiveStart) || !strings.HasSuffix(trimmed, telegramPhotoDirectiveEnd) {
|
||||
if !strings.HasPrefix(trimmed, telegramPhotoDirectiveStart) || !strings.HasSuffix(trimmed, telegramDirectiveEnd) {
|
||||
return assistantPhotoDirective{}, false
|
||||
}
|
||||
raw := strings.TrimSuffix(strings.TrimPrefix(trimmed, telegramPhotoDirectiveStart), telegramPhotoDirectiveEnd)
|
||||
raw := strings.TrimSuffix(strings.TrimPrefix(trimmed, telegramPhotoDirectiveStart), telegramDirectiveEnd)
|
||||
raw = strings.TrimSpace(raw)
|
||||
var directive assistantPhotoDirective
|
||||
if err := json.Unmarshal([]byte(raw), &directive); err != nil {
|
||||
@@ -1879,7 +1936,7 @@ func parseAssistantPhotoDirectiveLine(line string) (assistantPhotoDirective, boo
|
||||
return directive, true
|
||||
}
|
||||
|
||||
func (b *Bot) sendAssistantText(ctx context.Context, chatID int64, text string) error {
|
||||
func (b *Bot) sendAssistantText(ctx context.Context, threadID string, chatID int64, text string) error {
|
||||
for _, segment := range splitAssistantMessageSegments(text) {
|
||||
if segment.Text != "" && strings.TrimSpace(segment.Text) != "" {
|
||||
if err := b.sendLong(ctx, chatID, segment.Text); err != nil {
|
||||
@@ -1894,10 +1951,65 @@ func (b *Bot) sendAssistantText(ctx context.Context, chatID int64, text string)
|
||||
}
|
||||
}
|
||||
}
|
||||
if segment.ThreadRename != nil {
|
||||
if err := b.applyAssistantThreadRename(ctx, threadID, *segment.ThreadRename); err != nil {
|
||||
b.logger.Printf("apply assistant thread rename: %v", err)
|
||||
if sendErr := b.sendLong(ctx, chatID, "Could not rename thread: "+err.Error()); sendErr != nil {
|
||||
return sendErr
|
||||
}
|
||||
}
|
||||
}
|
||||
if segment.ThreadCWD != nil {
|
||||
if err := b.applyAssistantThreadCWD(ctx, threadID, *segment.ThreadCWD); err != nil {
|
||||
b.logger.Printf("apply assistant thread cwd: %v", err)
|
||||
if sendErr := b.sendLong(ctx, chatID, "Could not change thread cwd: "+err.Error()); sendErr != nil {
|
||||
return sendErr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bot) applyAssistantThreadRename(ctx context.Context, threadID string, directive assistantThreadRenameDirective) error {
|
||||
title := normalizeThreadTitle(directive.Title)
|
||||
if title == "" {
|
||||
return errors.New("thread title cannot be empty")
|
||||
}
|
||||
if err := b.codex.SetThreadName(ctx, threadID, title); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.store.SyncThreadTitleByCodexID(ctx, threadID, title)
|
||||
}
|
||||
|
||||
func (b *Bot) applyAssistantThreadCWD(ctx context.Context, threadID string, directive assistantThreadCWDDirective) error {
|
||||
cwd, err := store.ValidateWorkspacePath(directive.CWD)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := codexstate.SetThreadCWD(ctx, b.codexHome, b.codexStateDB, threadID, cwd); err != nil {
|
||||
return err
|
||||
}
|
||||
workspace, ok, err := b.workspaceForCodexCWD(ctx, cwd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
thread, err := b.store.GetThreadByCodexID(ctx, threadID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := b.store.SyncThreadWorkspace(ctx, thread.TelegramUserID, thread.ID, workspace.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.store.SetSessionWorkspace(ctx, thread.TelegramUserID, workspace.ID)
|
||||
}
|
||||
|
||||
func (b *Bot) sendAssistantPhoto(ctx context.Context, chatID int64, directive assistantPhotoDirective) error {
|
||||
path := strings.TrimSpace(directive.Path)
|
||||
if path == "" {
|
||||
@@ -1959,7 +2071,7 @@ func (b *Bot) flushAssistantMessage(ctx context.Context, threadID string) error
|
||||
state.assistant.Reset()
|
||||
b.mu.Unlock()
|
||||
|
||||
if err := b.sendAssistantText(ctx, chatID, text); err != nil {
|
||||
if err := b.sendAssistantText(ctx, threadID, chatID, text); err != nil {
|
||||
return err
|
||||
}
|
||||
b.markOutputSent(threadID)
|
||||
|
||||
Reference in New Issue
Block a user