tâches planifiées : prompts cron persistés, outil scheduler, conversations liées aux tâches, rattrapage des exécutions manquées au redémarrage (go-cron MissedRunOnce)

This commit is contained in:
fabien committed 2026-10-06 22:15:59 +02:00
1 parent bedb6c98e7
commit 058370830f
71 files changed
+1627 -100

No files matched your search

@@ -2,6 +2,7 @@ package turso
import (
"database/sql"
"fmt"
"log"
"strings"
@@ -30,7 +31,7 @@ func NewTursoChatRepository(db *sql.DB) *TursoChatRepository {
}
}
func (r *TursoChatRepository) ListChats(userID string, teamID string, page int, query string) ([]*domain.Chat, error) {
func (r *TursoChatRepository) ListChats(userID string, teamID string, page int, query string, taskID string) ([]*domain.Chat, error) {
where := "team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)"
args := []any{teamID, userID}
if query != "" {
@@ -41,6 +42,10 @@ func (r *TursoChatRepository) ListChats(userID string, teamID string, page int,
where += " and title like $3 escape '\\'"
args = append(args, "%"+escaped+"%")
}
if taskID != "" {
where += fmt.Sprintf(" and task_id=$%d", len(args)+1)
args = append(args, taskID)
}
return r.chatTable.Select(
orm.WithWhere(where, args...),
orm.WithOrder("_date_created desc"),
@@ -87,3 +87,7 @@ func (db *TursoDB) ChatRepository() domain.ChatRepository {
func (db *TursoDB) ToolRepository() domain.ToolRepository {
return NewTursoToolRepository(db.DB)
}
func (db *TursoDB) TaskRepository() domain.TaskRepository {
return NewTursoTaskRepository(db.DB)
}
@@ -25,7 +25,7 @@ func TestListChatsFilter(t *testing.T) {
create table user_teams (user_id text, team_id text);
insert into chats (id, team_id, user_id, agent_id, task_id, title, fresh_title, _date_created, _version)
values ('c1','t1','u1','','','Problème de timeout',0,'2026-01-01 10:00:00','v'),
('c2','t1','u1','','','Discussion navigateur',0,'2026-01-02 10:00:00','v'),
('c2','t1','u1','','task-9','Discussion navigateur',0,'2026-01-02 10:00:00','v'),
('c3','t1','u1','','','Recette de cuisine 100%',0,'2026-01-03 10:00:00','v');
insert into user_teams (user_id, team_id) values ('u1','t1');
insert into chats (id, team_id, user_id, agent_id, task_id, title, fresh_title, _date_created, _version)
@@ -40,14 +40,14 @@ func TestListChatsFilter(t *testing.T) {
repo := NewTursoChatRepository(db)
// sans filtre, page 0 : 20 conversations (28 en base)
p0, err := repo.ListChats("u1", "t1", 0, "")
p0, err := repo.ListChats("u1", "t1", 0, "", "")
if err != nil {
t.Fatal(err)
}
if len(p0) != 20 {
t.Fatalf("page 0: expected 20, got %d", len(p0))
}
p1, err := repo.ListChats("u1", "t1", 1, "")
p1, err := repo.ListChats("u1", "t1", 1, "", "")
if err != nil {
t.Fatal(err)
}
@@ -61,7 +61,7 @@ func TestListChatsFilter(t *testing.T) {
}
seen[c.ID] = true
}
p2, err := repo.ListChats("u1", "t1", 2, "")
p2, err := repo.ListChats("u1", "t1", 2, "", "")
if err != nil {
t.Fatal(err)
}
@@ -70,7 +70,7 @@ func TestListChatsFilter(t *testing.T) {
}
// filtre simple
res, err := repo.ListChats("u1", "t1", 0, "timeout")
res, err := repo.ListChats("u1", "t1", 0, "timeout", "")
if err != nil {
t.Fatal(err)
}
@@ -79,7 +79,7 @@ func TestListChatsFilter(t *testing.T) {
}
// jokers LIKE neutralisés : '100%' ne doit rien matcher de spécial
res, err = repo.ListChats("u1", "t1", 0, "100%")
res, err = repo.ListChats("u1", "t1", 0, "100%", "")
if err != nil {
t.Fatal(err)
}
@@ -88,7 +88,7 @@ func TestListChatsFilter(t *testing.T) {
}
// pas de résultat
res, err = repo.ListChats("u1", "t1", 0, "inexistant")
res, err = repo.ListChats("u1", "t1", 0, "inexistant", "")
if err != nil {
t.Fatal(err)
}
@@ -96,5 +96,14 @@ func TestListChatsFilter(t *testing.T) {
t.Fatalf("expected 0, got %d", len(res))
}
// filtre par tâche
taskRes, err := repo.ListChats("u1", "t1", 0, "", "task-9")
if err != nil {
t.Fatal(err)
}
if len(taskRes) != 1 || taskRes[0].ID != "c2" {
t.Fatalf("filter task-9: expected [c2], got %v", taskRes)
}
_ = domain.Chat{}
}
@@ -0,0 +1 @@
alter table tasks drop column repeat;
@@ -0,0 +1 @@
alter table tasks add column repeat integer not null default 1;
@@ -0,0 +1 @@
alter table tasks drop column last_datetime;
@@ -0,0 +1 @@
alter table tasks add column last_datetime numeric;
@@ -0,0 +1,85 @@
package turso
import (
"database/sql"
"log"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type TursoTaskRepository struct {
db *sql.DB
taskTable orm.Table[domain.Task]
}
func NewTursoTaskRepository(db *sql.DB) *TursoTaskRepository {
dialect := orm.NewSqliteDialect()
taskTable, err := orm.NewTable[domain.Task](db, dialect, "tasks")
if err != nil {
log.Fatalf("error creating task table")
}
return &TursoTaskRepository{
db: db,
taskTable: taskTable,
}
}
func (r *TursoTaskRepository) ListTasks(userID string, teamID string) ([]*domain.Task, error) {
return r.taskTable.Select(
orm.WithWhere(
"team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)",
teamID,
userID,
),
orm.WithOrder("_date_created desc"),
)
}
func (r *TursoTaskRepository) ListActiveTasks() ([]*domain.Task, error) {
return r.taskTable.Select(
orm.WithWhere("status=$1", domain.TaskStatusActive),
)
}
func (r *TursoTaskRepository) GetTaskByID(userID string, teamID string, id string) (*domain.Task, error) {
return r.taskTable.SelectOne(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
func (r *TursoTaskRepository) CreateTask(task *domain.Task) (*domain.Task, error) {
return r.taskTable.Insert(task)
}
func (r *TursoTaskRepository) UpdateTask(task *domain.Task) (*domain.Task, error) {
return r.taskTable.Update(task)
}
func (r *TursoTaskRepository) DeleteTask(userID string, teamID string, id string) error {
return r.taskTable.DeleteWhere(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
// FirstUserOfTeam retourne le premier utilisateur d'une équipe. Les tâches
// planifiées ne portent pas d'utilisateur : c'est lui qui porte la conversation
// générée lors de l'exécution.
func (r *TursoTaskRepository) FirstUserOfTeam(teamID string) (string, error) {
var userID string
err := r.db.QueryRow(
"select user_id from user_teams where team_id=$1 order by _date_created asc limit 1",
teamID,
).Scan(&userID)
if err != nil {
return "", err
}
return userID, nil
}