From 0bb8d1cb46e26d92757b4d7aaf681c963ea42392 Mon Sep 17 00:00:00 2001 From: Fabien Masson Date: Wed, 2 Sep 2026 21:50:26 +0200 Subject: [PATCH] correction des pb de space : le spaceId est maintenant dans l'url uniquement --- backend/cmd/serve.go | 3 + backend/internal/adapter/database/factory.go | 1 + .../internal/adapter/database/orm/builder.go | 84 ++++ .../adapter/database/orm/dialect_sqlite.go | 289 ++++++++++++++ backend/internal/adapter/database/orm/orm.go | 373 ++++++++++++++++++ .../internal/adapter/database/orm/utility.go | 21 + .../database/turso/agent_repository.go | 63 +++ backend/internal/adapter/database/turso/db.go | 6 +- .../adapter/database/turso/file_repository.go | 8 +- .../migrations/000001_create_tables.down.sql | 1 + .../migrations/000001_create_tables.up.sql | 21 + .../database/turso/provider_repository.go | 37 +- .../database/turso/settings_repository.go | 8 +- .../adapter/database/turso/user_repository.go | 37 +- backend/internal/adapter/llm/llm.go | 10 +- backend/internal/domain/agent.go | 28 ++ backend/internal/domain/select.go | 10 +- backend/internal/http/handlers/agent.go | 100 +++++ backend/internal/http/handlers/provider.go | 16 +- backend/internal/http/router.go | 3 + backend/internal/service/agent/service.go | 64 +++ backend/internal/service/provider/service.go | 23 ++ backend/lassistanoque.db-wal | Bin 358472 -> 57712 bytes backend/web/src/lib/api/endpoints/agent.ts | 10 + backend/web/src/lib/api/endpoints/auth.ts | 5 - backend/web/src/lib/api/endpoints/provider.ts | 16 +- backend/web/src/lib/components/Sidebar.svelte | 12 +- backend/web/src/lib/state/space.svelte.ts | 3 +- backend/web/src/lib/types/api.ts | 24 +- .../src/routes/(authenticated)/+layout.svelte | 3 +- .../web/src/routes/(authenticated)/+layout.ts | 3 +- .../[space]/settings/agents/+page.svelte | 145 ++++++- .../[space]/settings/agents/+page.ts | 13 +- .../[space]/settings/agents/AgentForm.svelte | 88 +++++ .../[space]/settings/models/+page.svelte | 10 +- .../[space]/settings/models/+page.ts | 5 +- .../settings/models/ProviderForm.svelte | 12 +- .../(authenticated)/profile/+page.svelte | 4 +- backend/web/src/routes/layout.css | 6 +- go.mod | 1 - go.sum | 18 - 41 files changed, 1463 insertions(+), 121 deletions(-) create mode 100644 backend/internal/adapter/database/orm/builder.go create mode 100644 backend/internal/adapter/database/orm/dialect_sqlite.go create mode 100644 backend/internal/adapter/database/orm/orm.go create mode 100644 backend/internal/adapter/database/orm/utility.go create mode 100644 backend/internal/adapter/database/turso/agent_repository.go create mode 100644 backend/internal/domain/agent.go create mode 100644 backend/internal/http/handlers/agent.go create mode 100644 backend/internal/service/agent/service.go create mode 100644 backend/web/src/lib/api/endpoints/agent.ts create mode 100644 backend/web/src/routes/(authenticated)/[space]/settings/agents/AgentForm.svelte diff --git a/backend/cmd/serve.go b/backend/cmd/serve.go index 1629c98..0ad6614 100644 --- a/backend/cmd/serve.go +++ b/backend/cmd/serve.go @@ -11,6 +11,7 @@ import ( "trankilou.fr/lassistanoque/backend/internal/adapter/llm" "trankilou.fr/lassistanoque/backend/internal/adapter/security" "trankilou.fr/lassistanoque/backend/internal/http" + "trankilou.fr/lassistanoque/backend/internal/service/agent" "trankilou.fr/lassistanoque/backend/internal/service/auth" "trankilou.fr/lassistanoque/backend/internal/service/provider" "trankilou.fr/lassistanoque/backend/internal/service/storage" @@ -71,6 +72,7 @@ func runServe() { userService := user.NewService(db.UserRepository()) storageService := storage.NewService(store) providerService := provider.NewService(db.ProviderRepository(), db.UserRepository(), llmEngine) + agentService := agent.NewService(db.AgentRepository(), db.UserRepository()) // http server router := http.NewRouter(http.Dependencies{ @@ -79,6 +81,7 @@ func runServe() { UserService: userService, TokenManager: tokenManager, ProviderService: providerService, + AgentService: agentService, }) if err := router.Start(); err != nil { diff --git a/backend/internal/adapter/database/factory.go b/backend/internal/adapter/database/factory.go index 4a8a48c..22d79fd 100644 --- a/backend/internal/adapter/database/factory.go +++ b/backend/internal/adapter/database/factory.go @@ -13,6 +13,7 @@ type Database interface { UserRepository() domain.UserRepository FileRepository() domain.FileRepository ProviderRepository() domain.ProviderRepository + AgentRepository() domain.AgentRepository Migrate() error Close() } diff --git a/backend/internal/adapter/database/orm/builder.go b/backend/internal/adapter/database/orm/builder.go new file mode 100644 index 0000000..d8a4095 --- /dev/null +++ b/backend/internal/adapter/database/orm/builder.go @@ -0,0 +1,84 @@ +package orm + +import ( + "fmt" + "strings" +) + +func buildInsertSql(tablename string, fields []dbfield, dialect Dialect) string { + cols := make([]string, len(fields)) + placeholders := make([]string, len(fields)) + for i, f := range fields { + cols[i] = f.dbname + placeholders[i] = fmt.Sprintf("$%d", i+1) + } + return fmt.Sprintf( + "insert into %s (%s) values (%s)", + tablename, + strings.Join(cols, ","), + strings.Join(placeholders, ","), + ) +} + +func buildUpdateSql(tablename string, fields []dbfield, dialect Dialect) string { + predicates := make([]string, len(fields)-1) + var where string + i := 0 + for _, f := range fields { + if f.dbname == "id" { + where = fmt.Sprintf("id=$%d and _version=$%d", len(fields), len(fields)+1) + } else { + predicates[i] = fmt.Sprintf("%s=$%d", f.dbname, i+1) + i++ + } + } + return fmt.Sprintf( + "update %s set %s where %s", + tablename, + strings.Join(predicates, ","), + where, + ) +} + +func buildSelectbyidSql(tablename string, fields []dbfield, dialect Dialect) string { + columns := make([]string, len(fields)) + var where string + for i, f := range fields { + columns[i] = fmt.Sprintf("%s", f.dbname) + } + where = "id=$1" + return fmt.Sprintf( + "select %s from %s where %s", + strings.Join(columns, ","), + tablename, + where, + ) +} + +func buildDeletebyidSql(tablename string, fields []dbfield, dialect Dialect) string { + where := "id=$1" + return fmt.Sprintf( + "delete from %s where %s", + tablename, + where, + ) +} + +func buildSelectWhere(tablename string, fields []dbfield, dialect Dialect) string { + columns := make([]string, len(fields)) + for i, f := range fields { + columns[i] = fmt.Sprintf("%s", f.dbname) + } + return fmt.Sprintf( + "select %s from %s", + strings.Join(columns, ","), + tablename, + ) +} + +func buildDeleteWhere(tablename string, fields []dbfield, dialect Dialect) string { + return fmt.Sprintf( + "delete from %s", + tablename, + ) +} diff --git a/backend/internal/adapter/database/orm/dialect_sqlite.go b/backend/internal/adapter/database/orm/dialect_sqlite.go new file mode 100644 index 0000000..25c38d9 --- /dev/null +++ b/backend/internal/adapter/database/orm/dialect_sqlite.go @@ -0,0 +1,289 @@ +package orm + +import ( + "database/sql" + "fmt" + "reflect" + "strings" + "time" +) + +type SqliteDialect struct{} + +func NewSqliteDialect() *SqliteDialect { + return &SqliteDialect{} +} + +func convertType(f dbfield) string { + switch f.stype { + case "string", "*string": + return "text" + case "bool", "int", "*int", "time.Time", "*time.Time": + return "numeric" + case "[]byte": + return "blob" + } + return "text" +} + +func defaultValue(f dbfield) string { + if f.dbname == "_date_created" { + return "current_timestamp" + } + switch f.stype { + case "string": + return "''" + case "bool", "int", "time.Time": + return "0" + } + return "''" +} + +func (d *SqliteDialect) TableExists(db *sql.DB, tableName string) (bool, error) { + sql := "select name from sqlite_schema where type='table' and name=$1" + rows, err := db.Query(sql, tableName) + if err != nil { + return false, err + } + if rows.Next() { + rows.Close() + return true, nil + } + return false, nil +} + +func (d *SqliteDialect) ColumnSpec(f dbfield) (string, error) { + if f.dbname == "id" { + return "id text not null primary key", nil + } + if strings.HasPrefix(f.stype, "*") || strings.HasPrefix(f.stype, "[]") { + return fmt.Sprintf("%s %s", f.dbname, convertType(f)), nil + } + return fmt.Sprintf("%s %s not null default %s", f.dbname, convertType(f), defaultValue(f)), nil +} + +// ScanResult scans a single row into the provided fields slice. +// For SQLite, this handles time.Time fields which are returned as strings. +func (d *SqliteDialect) ScanResult(row interface{ Scan(dest ...any) error }, fields []any, fieldTypes []dbfield) error { + // SQLite returns time.Time as strings, so we need special handling + // Check if any field is a time type + hasTimeField := false + for _, f := range fieldTypes { + if f.stype == "time.Time" || f.stype == "*time.Time" { + hasTimeField = true + break + } + } + + if !hasTimeField { + // No time fields, use standard scan + return row.Scan(fields...) + } + + // We have time fields - need custom handling + // Create temp destinations for all fields + tempFields := make([]any, len(fields)) + fieldInfos := make([]struct { + index int + dest any + isTime bool + isTimePtr bool + }, len(fields)) + + for i, f := range fieldTypes { + isTime := f.stype == "time.Time" + isTimePtr := f.stype == "*time.Time" + + fieldInfos[i] = struct { + index int + dest any + isTime bool + isTimePtr bool + }{ + index: i, + dest: fields[i], + isTime: isTime, + isTimePtr: isTimePtr, + } + + if isTime || isTimePtr { + var s sql.NullString + tempFields[i] = &s + } else { + tempFields[i] = fields[i] + } + } + + if err := row.Scan(tempFields...); err != nil { + return err + } + + // Copy time values back to original destinations + for _, info := range fieldInfos { + if info.isTime || info.isTimePtr { + s := tempFields[info.index].(*sql.NullString) + if s.Valid { + // Try to parse the string as time + t, err := tryParseTime(s.String) + if err != nil { + return fmt.Errorf("failed to parse time field: %w", err) + } + + // Set the value in the original destination + destVal := reflect.ValueOf(info.dest).Elem() + if info.isTime { + // dest is *time.Time, set the value + destVal.Set(reflect.ValueOf(t)) + } else if info.isTimePtr { + // dest is **time.Time, allocate and set + ptr := reflect.New(destVal.Type().Elem()) + ptr.Elem().Set(reflect.ValueOf(t)) + destVal.Set(ptr) + } + } + } + } + + return nil +} + +// ScanRows scans multiple rows using the dialect-specific logic +func (d *SqliteDialect) ScanRows(rows *sql.Rows, fieldDest func() []any, fieldTypes []dbfield) ([]any, error) { + // Check if any field is a time type + hasTimeField := false + for _, f := range fieldTypes { + if f.stype == "time.Time" || f.stype == "*time.Time" { + hasTimeField = true + break + } + } + + if !hasTimeField { + // No time fields, use standard scan + var results []any + for rows.Next() { + fields := fieldDest() + if err := rows.Scan(fields...); err != nil { + return nil, err + } + results = append(results, fields...) + } + if err := rows.Err(); err != nil { + return nil, err + } + return results, nil + } + + // We have time fields - need custom handling + var results []any + for rows.Next() { + fields := fieldDest() + + // Create temp destinations for all fields + tempFields := make([]any, len(fields)) + fieldInfos := make([]struct { + index int + dest any + isTime bool + isTimePtr bool + }, len(fields)) + + for i, f := range fieldTypes { + isTime := f.stype == "time.Time" + isTimePtr := f.stype == "*time.Time" + + fieldInfos[i] = struct { + index int + dest any + isTime bool + isTimePtr bool + }{ + index: i, + dest: fields[i], + isTime: isTime, + isTimePtr: isTimePtr, + } + + if isTime || isTimePtr { + var s sql.NullString + tempFields[i] = &s + } else { + tempFields[i] = fields[i] + } + } + + if err := rows.Scan(tempFields...); err != nil { + return nil, err + } + + // Copy time values back to original destinations + for _, info := range fieldInfos { + if info.isTime || info.isTimePtr { + s := tempFields[info.index].(*sql.NullString) + if s.Valid { + t, err := tryParseTime(s.String) + if err != nil { + return nil, fmt.Errorf("failed to parse time field: %w", err) + } + + // Set the value in the original destination + destVal := reflect.ValueOf(info.dest).Elem() + if info.isTime { + // dest is *time.Time, set the value + destVal.Set(reflect.ValueOf(t)) + } else if info.isTimePtr { + // dest is **time.Time, allocate and set + ptr := reflect.New(destVal.Type().Elem()) + ptr.Elem().Set(reflect.ValueOf(t)) + destVal.Set(ptr) + } + } + } + } + + results = append(results, fields...) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return results, nil +} + +// tryParseTime attempts to parse a time string in various common formats +// SQLite driver returns time as "2006-01-02 15:04:05.999999999 -0700 MST m=+0.000000000" +// We need to strip the monotonic clock part (m=...) before parsing +func tryParseTime(s string) (time.Time, error) { + // Remove monotonic clock part if present + // Format: "2006-01-02 15:04:05.999999999 -0700 MST m=+0.000000000" + if idx := strings.Index(s, " m="); idx != -1 { + s = s[:idx] + } + + // Try common formats + formats := []string{ + time.RFC3339, + time.RFC3339Nano, + "2006-01-02T15:04:05Z07:00", + "2006-01-02 15:04:05.999999999-07:00", + "2006-01-02 15:04:05", + "2006-01-02T15:04:05", + "2006-01-02 15:04:05+00:00", + // Go's default time.String() format (without monotonic clock) + "2006-01-02 15:04:05.999999999 -0700 MST", + // SQLite without timezone + "2006-01-02 15:04:05.999999", + } + + var err error + var t time.Time + for _, format := range formats { + t, err = time.Parse(format, s) + if err == nil { + return t, nil + } + } + + return time.Time{}, fmt.Errorf("unable to parse time: %s", s) +} diff --git a/backend/internal/adapter/database/orm/orm.go b/backend/internal/adapter/database/orm/orm.go new file mode 100644 index 0000000..dc14517 --- /dev/null +++ b/backend/internal/adapter/database/orm/orm.go @@ -0,0 +1,373 @@ +package orm + +import ( + "database/sql" + "fmt" + "reflect" + "strings" + "time" +) + +type dbfield struct { + name string + stype string + dbname string +} + +type Dialect interface { + TableExists(db *sql.DB, tableName string) (bool, error) + ColumnSpec(f dbfield) (string, error) + ScanResult(row interface{ Scan(dest ...any) error }, fields []any, fieldTypes []dbfield) error +} + +type Table[T any] struct { + db *sql.DB + dialect Dialect + tablename string + fields []dbfield + insertsql string + updatesql string + selectbyid string + deletebyid string + selectwhere string + deletewhere string +} + +func NewTable[T any](db *sql.DB, dialect Dialect, tablename string) (Table[T], error) { + + var instance T + + t := reflect.TypeOf(instance) + + fields := make([]dbfield, 0) + for field := range t.Fields() { + f := dbfield{ + name: field.Name, + stype: field.Type.String(), + dbname: field.Tag.Get("db"), + } + fields = append(fields, f) + } + + return Table[T]{ + db: db, + dialect: dialect, + tablename: tablename, + fields: fields, + insertsql: buildInsertSql(tablename, fields, dialect), + updatesql: buildUpdateSql(tablename, fields, dialect), + selectbyid: buildSelectbyidSql(tablename, fields, dialect), + deletebyid: buildDeletebyidSql(tablename, fields, dialect), + selectwhere: buildSelectWhere(tablename, fields, dialect), + deletewhere: buildDeleteWhere(tablename, fields, dialect), + }, nil +} + +func (t Table[T]) Sync() error { + if exists, err := t.dialect.TableExists(t.db, t.tablename); exists { + return nil + } else if err != nil { + return err + } + + columns := make([]string, len(t.fields)) + for i, f := range t.fields { + c, err := t.dialect.ColumnSpec(f) + if err != nil { + return err + } + columns[i] = c + } + sql := fmt.Sprintf("create table %s (%s)", t.tablename, strings.Join(columns, ",")) + _, err := t.db.Exec(sql) + if err != nil { + return err + } + return nil +} + +func (t Table[T]) Get(id string) (*T, error) { + row := t.db.QueryRow(t.selectbyid, id) + if row == nil { + return nil, sql.ErrNoRows + } + var obj T + + // Create field destinations + ptrVal := reflect.ValueOf(&obj).Elem() + fields := make([]any, len(t.fields)) + for i, f := range t.fields { + fieldValue := ptrVal.FieldByName(f.name) + if !fieldValue.IsValid() { + return nil, fmt.Errorf("field %s not found in struct", f.name) + } + fields[i] = fieldValue.Addr().Interface() + } + + // Use dialect to scan with proper type handling + if err := t.dialect.ScanResult(row, fields, t.fields); err != nil { + if err == sql.ErrNoRows { + return nil, sql.ErrNoRows + } + return nil, err + } + + return &obj, nil +} + +func (t Table[T]) SelectOne(where string, args ...any) (*T, error) { + query := t.selectwhere + " where " + where + " limit 1" + row := t.db.QueryRow(query, args...) + if row == nil { + return nil, sql.ErrNoRows + } + + var obj T + ptrVal := reflect.ValueOf(&obj).Elem() + fields := make([]any, len(t.fields)) + for i, f := range t.fields { + fieldValue := ptrVal.FieldByName(f.name) + if !fieldValue.IsValid() { + return nil, fmt.Errorf("field %s not found in struct", f.name) + } + fields[i] = fieldValue.Addr().Interface() + } + + if err := t.dialect.ScanResult(row, fields, t.fields); err != nil { + if err == sql.ErrNoRows { + return nil, sql.ErrNoRows + } + return nil, err + } + + return &obj, nil +} + +type query struct { + where string + args []any + orderby string + pagenum int + pagesize int +} + +type Option func(*query) + +func WithWhere(where string, args ...any) Option { + return func(q *query) { + q.where = where + q.args = args + } +} + +func WithOrder(orderby string) Option { + return func(q *query) { + q.orderby = orderby + } +} + +func WithPagination(pagenum, pagesize int) Option { + return func(q *query) { + q.pagenum = pagenum + q.pagesize = pagesize + } +} + +func (t Table[T]) Select(options ...Option) ([]*T, error) { + q := &query{} + for _, o := range options { + o(q) + } + query := t.selectwhere + if q.where != "" { + query += " where " + q.where + } + if q.orderby != "" { + query += " order by " + q.orderby + } + if q.pagesize != 0 { + query += fmt.Sprintf(" limit %d", q.pagesize) + if q.pagenum != 0 { + query += fmt.Sprintf(" offset %d", q.pagesize*q.pagenum) + } + } + + var err error + var rows *sql.Rows + if q.args == nil { + rows, err = t.db.Query(query) + } else { + rows, err = t.db.Query(query, q.args...) + } + + if err != nil { + return nil, err + } + defer rows.Close() + + var results []*T + + for rows.Next() { + var obj T + ptrVal := reflect.ValueOf(&obj).Elem() + fields := make([]any, len(t.fields)) + for i, f := range t.fields { + fieldValue := ptrVal.FieldByName(f.name) + if !fieldValue.IsValid() { + return nil, fmt.Errorf("field %s not found in struct", f.name) + } + fields[i] = fieldValue.Addr().Interface() + } + + // Use dialect to scan with proper type handling + // *sql.Rows implements Scanner interface + if err := t.dialect.ScanResult(rows, fields, t.fields); err != nil { + return nil, err + } + results = append(results, &obj) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return results, nil +} + +func (t Table[T]) Delete(id string) error { + _, err := t.db.Exec(t.deletebyid, id) + return err +} + +func (t Table[T]) DeleteWhere(where string, args ...any) error { + query := t.deletewhere + if where != "" { + query += " where " + where + } + _, err := t.db.Exec(query, args...) + return err +} + +func (t Table[T]) Insert(value *T) (*T, error) { + // Get a settable reflect value of the struct + // value is *T, &value is **T, .Elem() gives *T (settables), .Elem() gives T (settables) + val := reflect.ValueOf(&value).Elem().Elem() + + // Auto-fill ID field if it exists and is empty + if fieldID := val.FieldByName("ID"); fieldID.IsValid() && fieldID.CanSet() && fieldID.Kind() == reflect.String { + if fieldID.String() == "" { + fieldID.SetString(GenID()) + } + } + + // Auto-fill VersionId field if it exists and is empty + if fieldVersion := val.FieldByName("VersionId"); fieldVersion.IsValid() && fieldVersion.CanSet() && fieldVersion.Kind() == reflect.String { + if fieldVersion.String() == "" { + fieldVersion.SetString(GenID()) + } + } + + // Auto-fill DateCreated or DateCreate field if it exists + now := time.Now() + if fieldCreate := val.FieldByName("DateCreated"); fieldCreate.IsValid() && fieldCreate.CanSet() && fieldCreate.Type() == reflect.TypeOf(now) { + fieldCreate.Set(reflect.ValueOf(now)) + } else if fieldCreate := val.FieldByName("DateCreate"); fieldCreate.IsValid() && fieldCreate.CanSet() && fieldCreate.Type() == reflect.TypeOf(now) { + fieldCreate.Set(reflect.ValueOf(now)) + } + + vals := make([]any, len(t.fields)) + for i, f := range t.fields { + fieldValue := val.FieldByName(f.name) + if !fieldValue.IsValid() { + return nil, fmt.Errorf("field %s not found in struct", f.name) + } + vals[i] = fieldValue.Interface() + } + + _, err := t.db.Exec(t.insertsql, vals...) + if err != nil { + return nil, err + } + + return value, nil +} + +func (t Table[T]) Update(value *T) (*T, error) { + // Get a settable reflect value of the struct + val := reflect.ValueOf(&value).Elem().Elem() + + // Save old VersionId for WHERE clause + versionField := val.FieldByName("VersionId") + if !versionField.IsValid() { + return nil, fmt.Errorf("VersionId field not found") + } + oldVersionId := versionField.String() + + // Generate new VersionId for optimistic locking + newVersionId := GenID() + versionField.SetString(newVersionId) + + // Update DateUpdated field if it exists + if fieldUpdated := val.FieldByName("DateUpdated"); fieldUpdated.IsValid() && fieldUpdated.CanSet() { + if fieldUpdated.Kind() == reflect.Ptr { + now := time.Now() + if fieldUpdated.IsNil() { + fieldUpdated.Set(reflect.New(fieldUpdated.Type().Elem())) + } + fieldUpdated.Elem().Set(reflect.ValueOf(now)) + } else if fieldUpdated.Type() == reflect.TypeOf(time.Time{}) { + fieldUpdated.Set(reflect.ValueOf(time.Now())) + } + } + + // Build the values slice for the prepared statement + // UPDATE SQL expects: SET fields (excluding ID), then WHERE: ID and VersionId (old version) + vals := make([]any, 0, len(t.fields)) + + // Add all fields except ID for SET clause (includes new VersionId) + for _, f := range t.fields { + if f.dbname == "id" { + continue // Skip ID field for SET clause + } + fieldValue := val.FieldByName(f.name) + if !fieldValue.IsValid() { + return nil, fmt.Errorf("field %s not found in struct", f.name) + } + vals = append(vals, fieldValue.Interface()) + } + + // Add ID and OLD VersionId at the end for WHERE clause + idField := val.FieldByName("ID") + + if !idField.IsValid() { + return nil, fmt.Errorf("ID field not found") + } + + vals = append(vals, idField.Interface(), oldVersionId) + + result, err := t.db.Exec(t.updatesql, vals...) + if err != nil { + return nil, err + } + + // Check if any row was affected + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, err + } + + if rowsAffected == 0 { + return nil, fmt.Errorf("no rows affected: record not found or version mismatch") + } + + return value, nil +} + +func (t Table[T]) Debug() { + fmt.Println("insert: ", t.insertsql) + fmt.Println("update: ", t.updatesql) + fmt.Println("selectbyid: ", t.selectbyid) + fmt.Println("deletebyid: ", t.deletebyid) + fmt.Println("selectwhere: ", t.selectwhere) + fmt.Println("deletewhere: ", t.deletewhere) +} diff --git a/backend/internal/adapter/database/orm/utility.go b/backend/internal/adapter/database/orm/utility.go new file mode 100644 index 0000000..a4bc51a --- /dev/null +++ b/backend/internal/adapter/database/orm/utility.go @@ -0,0 +1,21 @@ +package orm + +import ( + "crypto/rand" + "encoding/hex" + + "github.com/sixafter/nanoid" +) + +func GenID() string { + id, err := nanoid.New() + if err != nil { + // Fallback to random hex + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + return "fallback-id" + } + return hex.EncodeToString(b) + } + return id.String() +} diff --git a/backend/internal/adapter/database/turso/agent_repository.go b/backend/internal/adapter/database/turso/agent_repository.go new file mode 100644 index 0000000..b891eb9 --- /dev/null +++ b/backend/internal/adapter/database/turso/agent_repository.go @@ -0,0 +1,63 @@ +package turso + +import ( + "database/sql" + "log" + + "trankilou.fr/lassistanoque/backend/internal/adapter/database/orm" + "trankilou.fr/lassistanoque/backend/internal/domain" +) + +type TursoAgentRepository struct { + db *sql.DB + AgentTable orm.Table[domain.Agent] +} + +func NewTursoAgentRepository(db *sql.DB) *TursoAgentRepository { + dialect := orm.NewSqliteDialect() + AgentTable, err := orm.NewTable[domain.Agent](db, dialect, "agents") + if err != nil { + log.Fatalf("error creating lasebuche agent table") + } + + return &TursoAgentRepository{ + db: db, + AgentTable: AgentTable, + } +} + +func (r *TursoAgentRepository) ListAgents(userID string, teamID string) ([]*domain.Agent, error) { + return r.AgentTable.Select( + orm.WithWhere( + "team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)", + teamID, + userID, + ), + ) +} + +func (r *TursoAgentRepository) GetAgent(userID string, teamID string, id string) (*domain.Agent, error) { + return r.AgentTable.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 *TursoAgentRepository) CreateAgent(userID string, agent *domain.Agent) (*domain.Agent, error) { + return r.AgentTable.Insert(agent) +} + +func (r *TursoAgentRepository) UpdateAgent(userID string, agent *domain.Agent) (*domain.Agent, error) { + return r.AgentTable.Update(agent) +} + +func (r *TursoAgentRepository) DeleteAgent(userID string, teamID string, id string) error { + return r.AgentTable.DeleteWhere( + "id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)", + id, + teamID, + userID, + ) +} diff --git a/backend/internal/adapter/database/turso/db.go b/backend/internal/adapter/database/turso/db.go index 5ed7968..2169ca4 100644 --- a/backend/internal/adapter/database/turso/db.go +++ b/backend/internal/adapter/database/turso/db.go @@ -69,5 +69,9 @@ func (db *TursoDB) FileRepository() domain.FileRepository { } func (db *TursoDB) ProviderRepository() domain.ProviderRepository { - return NewTursoModelRepository(db.DB) + return NewTursoProviderRepository(db.DB) +} + +func (db *TursoDB) AgentRepository() domain.AgentRepository { + return NewTursoAgentRepository(db.DB) } diff --git a/backend/internal/adapter/database/turso/file_repository.go b/backend/internal/adapter/database/turso/file_repository.go index 5c3092c..bdfbb68 100644 --- a/backend/internal/adapter/database/turso/file_repository.go +++ b/backend/internal/adapter/database/turso/file_repository.go @@ -6,19 +6,19 @@ import ( "log" "time" - "gitea.trankilou.fr/fabien/lasebuche" + "trankilou.fr/lassistanoque/backend/internal/adapter/database/orm" "trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/utility" ) type TursoFileRepository struct { db *sql.DB - FileTable lasebuche.Table[domain.File] + FileTable orm.Table[domain.File] } func NewTursoFileRepository(db *sql.DB) *TursoFileRepository { - dialect := lasebuche.NewSqliteDialect() - fileTable, err := lasebuche.NewTable[domain.File](db, dialect, "settings") + dialect := orm.NewSqliteDialect() + fileTable, err := orm.NewTable[domain.File](db, dialect, "settings") if err != nil { log.Fatalf("error creating lasebuche team table") } diff --git a/backend/internal/adapter/database/turso/migrations/000001_create_tables.down.sql b/backend/internal/adapter/database/turso/migrations/000001_create_tables.down.sql index b62c63c..6f5807b 100644 --- a/backend/internal/adapter/database/turso/migrations/000001_create_tables.down.sql +++ b/backend/internal/adapter/database/turso/migrations/000001_create_tables.down.sql @@ -11,3 +11,4 @@ drop table tools; drop table tasks; drop table history; drop table files; +drop table agents; diff --git a/backend/internal/adapter/database/turso/migrations/000001_create_tables.up.sql b/backend/internal/adapter/database/turso/migrations/000001_create_tables.up.sql index 9671553..73f1605 100644 --- a/backend/internal/adapter/database/turso/migrations/000001_create_tables.up.sql +++ b/backend/internal/adapter/database/turso/migrations/000001_create_tables.up.sql @@ -141,3 +141,24 @@ create table files ( _date_updated numeric , _version text not null ); + +create table agents ( + id text not null primary key, + team_id text not null, + name text not null default '', + system_prompt text not null default '', + def_provider_id text not null default '', + def_model_id text not null default '', + tools_policy text not null default '', + sub_agents numeric not null default 0, + loop_strategy text not null default '', + max_iterations numeric not null default 1, + stopping_criteria text not null default '', + -- kb text not null default '', + -- tools text not null default '', + -- skills text not null default '', + -- channels text not null default '', + _date_created numeric not null default current_timestamp, + _date_updated numeric , + _version text not null +); diff --git a/backend/internal/adapter/database/turso/provider_repository.go b/backend/internal/adapter/database/turso/provider_repository.go index 6aea95c..9ef4fbe 100644 --- a/backend/internal/adapter/database/turso/provider_repository.go +++ b/backend/internal/adapter/database/turso/provider_repository.go @@ -4,37 +4,40 @@ import ( "database/sql" "log" - "gitea.trankilou.fr/fabien/lasebuche" + "trankilou.fr/lassistanoque/backend/internal/adapter/database/orm" "trankilou.fr/lassistanoque/backend/internal/domain" ) -type TursoModelRepository struct { +type TursoProviderRepository struct { db *sql.DB - providerTable lasebuche.Table[domain.Provider] + providerTable orm.Table[domain.Provider] } -func NewTursoModelRepository(db *sql.DB) *TursoModelRepository { - dialect := lasebuche.NewSqliteDialect() - providerTable, err := lasebuche.NewTable[domain.Provider](db, dialect, "providers") +func NewTursoProviderRepository(db *sql.DB) *TursoProviderRepository { + dialect := orm.NewSqliteDialect() + providerTable, err := orm.NewTable[domain.Provider](db, dialect, "providers") if err != nil { - log.Fatalf("error creating lasebuche team table") + log.Fatalf("error creating lasebuche provider table") } - return &TursoModelRepository{ + return &TursoProviderRepository{ db: db, providerTable: providerTable, } } -func (r *TursoModelRepository) ListProviders(userID string, teamID string) ([]*domain.Provider, error) { - return r.providerTable.SelectWhere( - "team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)", - teamID, - userID, +func (r *TursoProviderRepository) ListProviders(userID string, teamID string) ([]*domain.Provider, error) { + return r.providerTable.Select( + orm.WithWhere( + "team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)", + teamID, + userID, + ), + orm.WithOrder("name"), ) } -func (r *TursoModelRepository) GetProvider(userID string, teamID string, id string) (*domain.Provider, error) { +func (r *TursoProviderRepository) GetProvider(userID string, teamID string, id string) (*domain.Provider, error) { return r.providerTable.SelectOne( "id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)", id, @@ -43,15 +46,15 @@ func (r *TursoModelRepository) GetProvider(userID string, teamID string, id stri ) } -func (r *TursoModelRepository) CreateProvider(userID string, provider *domain.Provider) (*domain.Provider, error) { +func (r *TursoProviderRepository) CreateProvider(userID string, provider *domain.Provider) (*domain.Provider, error) { return r.providerTable.Insert(provider) } -func (r *TursoModelRepository) UpdateProvider(userID string, provider *domain.Provider) (*domain.Provider, error) { +func (r *TursoProviderRepository) UpdateProvider(userID string, provider *domain.Provider) (*domain.Provider, error) { return r.providerTable.Update(provider) } -func (r *TursoModelRepository) DeleteProvider(userID string, teamID string, id string) error { +func (r *TursoProviderRepository) DeleteProvider(userID string, teamID string, id string) error { return r.providerTable.DeleteWhere( "id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)", id, diff --git a/backend/internal/adapter/database/turso/settings_repository.go b/backend/internal/adapter/database/turso/settings_repository.go index 39e4918..9b090fc 100644 --- a/backend/internal/adapter/database/turso/settings_repository.go +++ b/backend/internal/adapter/database/turso/settings_repository.go @@ -4,18 +4,18 @@ import ( "database/sql" "log" - "gitea.trankilou.fr/fabien/lasebuche" + "trankilou.fr/lassistanoque/backend/internal/adapter/database/orm" "trankilou.fr/lassistanoque/backend/internal/domain" ) type TursoSettingsRepository struct { db *sql.DB - SettingsTable lasebuche.Table[domain.Settings] + SettingsTable orm.Table[domain.Settings] } func NewTursoSettingsRepository(db *sql.DB) *TursoSettingsRepository { - dialect := lasebuche.NewSqliteDialect() - settingsTable, err := lasebuche.NewTable[domain.Settings](db, dialect, "settings") + dialect := orm.NewSqliteDialect() + settingsTable, err := orm.NewTable[domain.Settings](db, dialect, "settings") if err != nil { log.Fatalf("error creating lasebuche team table") } diff --git a/backend/internal/adapter/database/turso/user_repository.go b/backend/internal/adapter/database/turso/user_repository.go index 4596b57..c5a8149 100644 --- a/backend/internal/adapter/database/turso/user_repository.go +++ b/backend/internal/adapter/database/turso/user_repository.go @@ -4,34 +4,33 @@ import ( "database/sql" "log" + "trankilou.fr/lassistanoque/backend/internal/adapter/database/orm" "trankilou.fr/lassistanoque/backend/internal/domain" - - "gitea.trankilou.fr/fabien/lasebuche" ) type TursoUserRepository struct { DB *sql.DB - UserTable lasebuche.Table[domain.User] - TeamTable lasebuche.Table[domain.Team] - UserTeamTable lasebuche.Table[domain.UserTeam] - AddressTable lasebuche.Table[domain.UserAddress] + UserTable orm.Table[domain.User] + TeamTable orm.Table[domain.Team] + UserTeamTable orm.Table[domain.UserTeam] + AddressTable orm.Table[domain.UserAddress] } func NewTursoUserRepository(db *sql.DB) *TursoUserRepository { - dialect := lasebuche.NewSqliteDialect() - userTable, err := lasebuche.NewTable[domain.User](db, dialect, "users") + dialect := orm.NewSqliteDialect() + userTable, err := orm.NewTable[domain.User](db, dialect, "users") if err != nil { log.Fatalf("error creating lasebuche user table") } - teamTable, err := lasebuche.NewTable[domain.Team](db, dialect, "teams") + teamTable, err := orm.NewTable[domain.Team](db, dialect, "teams") if err != nil { log.Fatalf("error creating lasebuche team table") } - userTeamTable, err := lasebuche.NewTable[domain.UserTeam](db, dialect, "user_teams") + userTeamTable, err := orm.NewTable[domain.UserTeam](db, dialect, "user_teams") if err != nil { log.Fatalf("error creating lasebuche team table") } - userAddressTable, err := lasebuche.NewTable[domain.UserAddress](db, dialect, "user_addresses") + userAddressTable, err := orm.NewTable[domain.UserAddress](db, dialect, "user_addresses") if err != nil { log.Fatalf("error creating lasebuche team table") } @@ -53,7 +52,7 @@ func (ur *TursoUserRepository) FindUserByEmail(email string) (*domain.User, erro } func (ur *TursoUserRepository) ListUsers() ([]*domain.User, error) { - return ur.UserTable.SelectWhere("") + return ur.UserTable.Select() } func (ur *TursoUserRepository) CreateUser(user *domain.User) (*domain.User, error) { @@ -73,7 +72,10 @@ func (ur *TursoUserRepository) FindTeam(userid string, teamid string) (*domain.T } func (ur *TursoUserRepository) ListTeams(userid string) ([]*domain.Team, error) { - return ur.TeamTable.SelectWhere("id in (select team_id from user_teams where user_id=$1)", userid) + return ur.TeamTable.Select( + orm.WithWhere("id in (select team_id from user_teams where user_id=$1)", userid), + orm.WithOrder("label asc"), + ) } func (ur *TursoUserRepository) CreateTeam(userid string, team *domain.Team) (*domain.Team, error) { @@ -108,7 +110,9 @@ func (ur *TursoUserRepository) FindUserTeam(userid string, teamid string) (*doma } func (ur *TursoUserRepository) ListUserTeams(userid string) ([]*domain.UserTeam, error) { - return ur.UserTeamTable.SelectWhere("user_id=$1", userid) + return ur.UserTeamTable.Select( + orm.WithWhere("user_id=$1", userid), + ) } func (ur *TursoUserRepository) CreateUserTeam(userid string, team *domain.UserTeam) (*domain.UserTeam, error) { @@ -124,7 +128,10 @@ func (ur *TursoUserRepository) DeleteUserTeam(userid string, teamid string) erro } func (ur *TursoUserRepository) ListUserAddresses(id string) ([]*domain.UserAddress, error) { - return ur.AddressTable.SelectWhere("user_id=$1", id) + return ur.AddressTable.Select( + orm.WithWhere("user_id=$1", id), + orm.WithOrder("type asc"), + ) } func (ur *TursoUserRepository) GetUserAddress(addrID string) (*domain.UserAddress, error) { diff --git a/backend/internal/adapter/llm/llm.go b/backend/internal/adapter/llm/llm.go index da3ceb1..031244d 100644 --- a/backend/internal/adapter/llm/llm.go +++ b/backend/internal/adapter/llm/llm.go @@ -12,11 +12,11 @@ import ( ) var providerTypes = []domain.Item{ - {ID: "anthropic", Text: "Anthropic"}, - {ID: "openai", Text: "OpenAI"}, - {ID: "ollama", Text: "Ollama"}, - {ID: "openaicomp", Text: "OpenAI compatible"}, - {ID: "openrouter", Text: "Openrouter"}, + {Value: "anthropic", Label: "Anthropic"}, + {Value: "openai", Label: "OpenAI"}, + {Value: "ollama", Label: "Ollama"}, + {Value: "openaicomp", Label: "OpenAI compatible"}, + {Value: "openrouter", Label: "Openrouter"}, } type AnyLLMEngine struct { diff --git a/backend/internal/domain/agent.go b/backend/internal/domain/agent.go new file mode 100644 index 0000000..0474f49 --- /dev/null +++ b/backend/internal/domain/agent.go @@ -0,0 +1,28 @@ +package domain + +import "time" + +type Agent struct { + ID string `db:"id" json:"id"` + TeamID string `db:"team_id" json:"teamId"` + Name string `db:"name" json:"name"` + SystemPrompt string `db:"system_prompt" json:"systemPrompt"` + DefaultProviderID string `db:"def_provider_id" json:"defaultProviderId"` + DefaultModelID string `db:"def_model_id" json:"defaultModelId"` + ToolsPolicy string `db:"tools_policy" json:"toolsPolicy"` + SubAgents bool `db:"sub_agents" json:"subAgents"` + LoopStrategy string `db:"loop_strategy" json:"loopStrategy"` + MaxIterations string `db:"max_iterations" json:"maxIterations"` + StoppingCriteria string `db:"stopping_criteria" json:"stoppingCriteria"` + DateCreated time.Time `db:"_date_created" json:"_date_created"` + DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"` + VersionId string `db:"_version" json:"_version"` +} + +type AgentRepository interface { + ListAgents(userID string, teamID string) ([]*Agent, error) + GetAgent(userID string, teamID string, id string) (*Agent, error) + CreateAgent(userID string, agent *Agent) (*Agent, error) + UpdateAgent(userID string, agent *Agent) (*Agent, error) + DeleteAgent(userID string, teamID string, id string) error +} diff --git a/backend/internal/domain/select.go b/backend/internal/domain/select.go index 8d77f39..68e9f3b 100644 --- a/backend/internal/domain/select.go +++ b/backend/internal/domain/select.go @@ -1,6 +1,12 @@ package domain type Item struct { - ID string `json:"id"` - Text string `json:"text"` + Value string `json:"value"` + Label string `json:"label"` +} + +type ComplexItem struct { + Value string `json:"value"` + Label string `json:"label"` + Group string `json:"group"` } diff --git a/backend/internal/http/handlers/agent.go b/backend/internal/http/handlers/agent.go new file mode 100644 index 0000000..3f8da44 --- /dev/null +++ b/backend/internal/http/handlers/agent.go @@ -0,0 +1,100 @@ +package handlers + +import ( + "net/http" + + "github.com/labstack/echo/v5" + "trankilou.fr/lassistanoque/backend/internal/domain" + "trankilou.fr/lassistanoque/backend/internal/service/agent" + "trankilou.fr/lassistanoque/backend/internal/service/auth" +) + +func NewAgentGroup(prefix string, e *echo.Group, service *agent.Service, middlewares ...echo.MiddlewareFunc) *echo.Group { + agentHandler := &AgentHandler{ + agentService: service, + } + agent := e.Group(prefix, middlewares...) + + agent.GET("/:space", agentHandler.ListAgents) + agent.GET("/:space/:agent", agentHandler.GetAgent) + agent.PUT("/:space", agentHandler.UpdateAgent) + agent.POST("/:space", agentHandler.CreateAgent) + agent.DELETE("/:space/:agent", agentHandler.DeleteAgent) + + return agent +} + +type AgentHandler struct { + agentService *agent.Service +} + +func (h *AgentHandler) ListAgents(c *echo.Context) error { + userID := c.Get(auth.ContextUserIDKey).(string) + teamID := c.Param("space") + agents, err := h.agentService.ListAgents(userID, teamID) + if err != nil { + c.Logger().Error("error listing agents", err) + return echo.NewHTTPError(http.StatusBadRequest, "error listing agents") + } + return c.JSON(http.StatusOK, agents) +} + +func (h *AgentHandler) GetAgent(c *echo.Context) error { + userID := c.Get(auth.ContextUserIDKey).(string) + teamID := c.Param("space") + agentID := c.Param("agent") + agent, err := h.agentService.GetAgent(userID, teamID, agentID) + if err != nil { + c.Logger().Error("error getting agent: %s", err) + return echo.NewHTTPError(http.StatusBadRequest, "error getting agent") + } + return c.JSON(http.StatusOK, agent) +} + +func (h *AgentHandler) CreateAgent(c *echo.Context) error { + userID := c.Get(auth.ContextUserIDKey).(string) + teamID := c.Param("space") + var agent domain.Agent + err := c.Bind(&agent) + if err != nil { + c.Logger().Error("error binding agent : %s", err) + return echo.NewHTTPError(http.StatusBadRequest, "error binding agent") + } + agent.TeamID = teamID + updagent, err := h.agentService.CreateAgent(userID, &agent) + if err != nil { + c.Logger().Error("error creating agent: %s", err) + return echo.NewHTTPError(http.StatusBadRequest, "error creating agent") + } + return c.JSON(http.StatusOK, updagent) +} + +func (h *AgentHandler) UpdateAgent(c *echo.Context) error { + userID := c.Get(auth.ContextUserIDKey).(string) + teamID := c.Param("space") + var agent domain.Agent + err := c.Bind(&agent) + if err != nil { + c.Logger().Error("error binding agent : %s", err) + return echo.NewHTTPError(http.StatusBadRequest, "error binding agent") + } + agent.TeamID = teamID + updagent, err := h.agentService.UpdateAgent(userID, &agent) + if err != nil { + c.Logger().Error("error updating agent: %s", err) + return echo.NewHTTPError(http.StatusBadRequest, "error updating agent") + } + return c.JSON(http.StatusOK, updagent) +} + +func (h *AgentHandler) DeleteAgent(c *echo.Context) error { + userID := c.Get(auth.ContextUserIDKey).(string) + teamID := c.Param("space") + agentID := c.Param("agent") + err := h.agentService.DeleteAgent(userID, teamID, agentID) + if err != nil { + c.Logger().Error("error creating agent: %s", err) + return echo.NewHTTPError(http.StatusBadRequest, "error creating agent") + } + return c.JSON(http.StatusOK, agentID) +} diff --git a/backend/internal/http/handlers/provider.go b/backend/internal/http/handlers/provider.go index fadffa0..753438d 100644 --- a/backend/internal/http/handlers/provider.go +++ b/backend/internal/http/handlers/provider.go @@ -17,6 +17,7 @@ func NewModelGroup(prefix string, e *echo.Group, service *provider.Service, midd model.GET("/providerTypes", modelHandler.ListProviderTypes) model.GET("/:space", modelHandler.ListProviders) + model.GET("/:space/models", modelHandler.ListProvidersModels) model.POST("/:space/avalable-models", modelHandler.ListAvailableModels) model.GET("/:space/:provider", modelHandler.GetProvider) model.PUT("/:space", modelHandler.UpdateProvider) @@ -116,8 +117,19 @@ func (h *ModelHandler) ListAvailableModels(c *echo.Context) error { provider.TeamID = teamID list, err := h.providerService.ListAvailableModels(c.Request().Context(), &provider) if err != nil { - c.Logger().Error("error listing models: %s", err) - return echo.NewHTTPError(http.StatusBadRequest, "error creating provider") + c.Logger().Error("error listing available models: %s", err) + return echo.NewHTTPError(http.StatusBadRequest, "error listing available models") + } + return c.JSON(http.StatusOK, list) +} + +func (h *ModelHandler) ListProvidersModels(c *echo.Context) error { + userID := c.Get(auth.ContextUserIDKey).(string) + teamID := c.Param("space") + list, err := h.providerService.ListProvidersModels(userID, teamID) + if err != nil { + c.Logger().Error("error listing providers models: %s", err) + return echo.NewHTTPError(http.StatusBadRequest, "error listing providers models") } return c.JSON(http.StatusOK, list) } diff --git a/backend/internal/http/router.go b/backend/internal/http/router.go index cd9cbe9..768bdae 100644 --- a/backend/internal/http/router.go +++ b/backend/internal/http/router.go @@ -10,6 +10,7 @@ import ( "github.com/labstack/echo/v5/middleware" "trankilou.fr/lassistanoque/backend/internal/config" "trankilou.fr/lassistanoque/backend/internal/http/handlers" + "trankilou.fr/lassistanoque/backend/internal/service/agent" "trankilou.fr/lassistanoque/backend/internal/service/auth" "trankilou.fr/lassistanoque/backend/internal/service/provider" "trankilou.fr/lassistanoque/backend/internal/service/storage" @@ -37,6 +38,7 @@ type Dependencies struct { AuthService *auth.Service UserService *user.Service ProviderService *provider.Service + AgentService *agent.Service TokenManager auth.TokenManager } @@ -74,6 +76,7 @@ func NewRouter(deps Dependencies) *Router { _ = handlers.NewUserGroup("/user", api, deps.UserService, deps.TokenManager.TokenMiddleware) _ = handlers.NewMiscGroup("/misc", api, deps.TokenManager.TokenMiddleware) _ = handlers.NewModelGroup("/provider", api, deps.ProviderService, deps.TokenManager.TokenMiddleware) + _ = handlers.NewAgentGroup("/agent", api, deps.AgentService, deps.TokenManager.TokenMiddleware) return &Router{ echo: e, diff --git a/backend/internal/service/agent/service.go b/backend/internal/service/agent/service.go new file mode 100644 index 0000000..ff78067 --- /dev/null +++ b/backend/internal/service/agent/service.go @@ -0,0 +1,64 @@ +package agent + +import ( + "fmt" + + "trankilou.fr/lassistanoque/backend/internal/domain" +) + +type Service struct { + repo domain.AgentRepository + repoUser domain.UserRepository +} + +func NewService( + repo domain.AgentRepository, + repoUser domain.UserRepository, +) *Service { + return &Service{ + repo, + repoUser, + } +} + +func (s *Service) ListAgents(userID string, teamID string) ([]*domain.Agent, error) { + return s.repo.ListAgents(userID, teamID) +} + +func (s *Service) GetAgent(userID string, teamID string, id string) (*domain.Agent, error) { + return s.repo.GetAgent(userID, teamID, id) +} + +func (s *Service) CreateAgent(userID string, Agent *domain.Agent) (*domain.Agent, error) { + + if _, err := s.repoUser.FindUserTeam(userID, Agent.TeamID); err != nil { + return nil, fmt.Errorf("error finding user in team: %s", err) + } + + return s.repo.CreateAgent(userID, Agent) +} + +func (s *Service) UpdateAgent(userID string, Agent *domain.Agent) (*domain.Agent, error) { + if _, err := s.repoUser.FindUserTeam(userID, Agent.TeamID); err != nil { + return nil, fmt.Errorf("error finding user in team: %s", err) + } + + if _, err := s.repo.GetAgent(userID, Agent.TeamID, Agent.ID); err != nil { + return nil, fmt.Errorf("error finding Agent: %s", err) + } + + return s.repo.UpdateAgent(userID, Agent) +} + +func (s *Service) DeleteAgent(userID string, teamID string, id string) error { + if _, err := s.repoUser.FindUserTeam(userID, teamID); err != nil { + return fmt.Errorf("error finding user in team: %s", err) + } + + if _, err := s.repo.GetAgent(userID, teamID, id); err != nil { + return fmt.Errorf("error finding Agent: %s", err) + } + + return s.repo.DeleteAgent(userID, teamID, id) + +} diff --git a/backend/internal/service/provider/service.go b/backend/internal/service/provider/service.go index 813dd36..6edc3cb 100644 --- a/backend/internal/service/provider/service.go +++ b/backend/internal/service/provider/service.go @@ -3,6 +3,7 @@ package provider import ( "context" "fmt" + "strings" "trankilou.fr/lassistanoque/backend/internal/domain" ) @@ -33,6 +34,28 @@ func (s *Service) ListProviders(userID string, teamID string) ([]*domain.Provide return s.repo.ListProviders(userID, teamID) } +func (s *Service) ListProvidersModels(userID string, teamID string) ([]*domain.ComplexItem, error) { + + items := make([]*domain.ComplexItem, 0) + + providers, err := s.repo.ListProviders(userID, teamID) + if err != nil { + return nil, err + } + for _, provider := range providers { + models := strings.Split(provider.Models, "|") + for _, m := range models { + items = append(items, &domain.ComplexItem{ + Group: provider.Name, + Value: provider.ID + "|" + m, + Label: m, + }) + } + } + + return items, nil +} + func (s *Service) GetProvider(userID string, teamID string, id string) (*domain.Provider, error) { return s.repo.GetProvider(userID, teamID, id) } diff --git a/backend/lassistanoque.db-wal b/backend/lassistanoque.db-wal index ba3d0a9858bfc9f23aa3765e72709cbf01801886..5c69bad7b723ecfff44ed3453c38ea759577ffe6 100644 GIT binary patch literal 57712 zcmeI*Uu+{s9S3md56~X6U4bg@5Y<_|hvUTAo!Qy-&N@iMu@gJC^T+X@IDT>0_S&&y zuN`|G$5Ev!ClEq?sc7K=kN~M_B~brdE7XTpD&hrDTk&+jOIwMTQY6GPLP(6yP8x%i z%UwCuQNOwK>Abs=XFj|0kgtC;qdxih8#eb}F4}Al+4?`v%|AW&>IWg^p>E*ye?R{( zhd6Xuyj(}ef4TD1#qV!ltJ}rFX~!*_bKP;vv1NZpyu}X$AOHafKmY;|fB*y_009V0 zxWM6sS*NOAxn{T99}usEO{MkijabwS#8=g&#$qQJZ-$n7)XPa)4|bZnS!K1BN%0k{ z5ULuw=?>KNPSHqqTt?f~Ff9P%WOXx82i!XQn6IFdadJbOG!d}67eZs59L&) z8mGfE5&2Zl?Iz*v~%u zJ&>4(6HchcWH#vbiS|m=M>L-zd#LynNnEB4E=dIa5mqU-YUX}3v0DxV^F4J(Jkle% zz*|o}dO0}v=dTWPfhpty6F%tCJP1Gl0uX=z1Rwwb2tWV=Cj^iSh>+sb&IPWI<^u6g zB^RL7r*My=N}5L5Fc#S4Q6pDa3njLKfnqvQTuW6qt^dgd-v8*v+rNL|pYIQHft&qY z;3nz?P7E^q7XlD~00bZa0SG_<0uX?}1Pgp7a)HCIjTQqW)caBlF#7#FWjm}W|)zl z8Fu?uj8rf);kkggAupet%W>Z71%CG9TduiZ|NWcD1xAJz-ar5X5P$##AOHafKmY;| zI2QuvITuiURQ3=>ktoq5KF9@niGy%Cqo+x#!j}C_%OA~mBgh4AFY|Ev`U3YpkKoa( zzx&gRU-;F7$OX>DkVhjS009U<00Izz00bZa0SJr;obOyfOo(Tca3*HN50?;(nIy(( zX8$0)m(JuuwS0AB5%mK7wIA=a?%-YJ0<+>FI^K`K(|u!EetqjFcE_4++FrETi_Yuz zmqs2lynz4&AOHafKmY;|fB*y_0D-#{INYB4qN-kc} zdZLk(`xLFe1^`pUT6;ue>Y(J{#3lWHNIphAMAl@nPCp%9lJK`8rQ%|ApK4ZUAu2^m zTPz^@@sa4qrIA~97@r>A$8X*L&W*icQu1sa?O(+`Tx=Pk5Qz@&DamzHJNb2erASk? zy>xuBwU{r9j*jF4uY`W#`N7+t`yO(EyBy@W90Cx400bZa0SG_<0uX?}DFVm^kP95= zJ7*0&K3OiHiJcidoDoG+H8KoxN|s1x>e^B@v6A)n;(7lntC-@k9oeU_er1n;NqYD_ z%p*8uK;skyAOHafKmY;|fB*y_0D%b=ILGq{`nB=gz{}fB*y_009U<00Iy=MF6=# zcvX*Zne8V7V`nRrOT|*z>a|YHE+E7w5uf7qP)=2>J&bz^p?tXP zAgz` zxNLvtl*bOIAOHafKmY;|fB*y_009U)s*J0zt~yOYs*ddp4E0U z#m~lqRr_Fpn%*fI$&O1{bxfCDHcac-YOsOCo%boct6ab(9^d1A3SWL^{vQtMyU$z@ z2WOnG*_QZB|6O1=QOTF3tnX&22O#e*S%(m9I2(L8UA6HLIUz|}N92-#l2Qb2)h5!Hn literal 358472 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 api.get(`/agent/${space}`), + getAgent: (space: string, id: string) => api.get(`/agent/${space}/${id}`), + createAgent: (space: string,data: Partial) => api.post(`/agent/${space}`, data), + updateAgent: (space: string,data: Partial) => api.put(`/agent/${space}`, data), + deleteAgent: (space: string,id: string) => api.delete(`/agent/${space}/${id}`), +}; diff --git a/backend/web/src/lib/api/endpoints/auth.ts b/backend/web/src/lib/api/endpoints/auth.ts index 198b306..753ec22 100644 --- a/backend/web/src/lib/api/endpoints/auth.ts +++ b/backend/web/src/lib/api/endpoints/auth.ts @@ -25,9 +25,4 @@ export const authApi = { register: (data: RegistrationData) => api.post('/auth/register', data), status: () => api.get('/auth/status'), login: (data: Credentials) => api.post('/auth/login', data), - // getAll: () => api.get('/users'), - // getById: (id: string) => api.get(`/users/${id}`), - // create: (data: Partial) => api.post('/users', data), - // update: (id: string, data: Partial) => api.put(`/users/${id}`, data), - // remove: (id: string) => api.delete(`/users/${id}`), }; diff --git a/backend/web/src/lib/api/endpoints/provider.ts b/backend/web/src/lib/api/endpoints/provider.ts index a263923..cf80d8d 100644 --- a/backend/web/src/lib/api/endpoints/provider.ts +++ b/backend/web/src/lib/api/endpoints/provider.ts @@ -1,13 +1,13 @@ import { api } from '../client'; -import type { Provider, Item } from "$lib/types/api" -import { space } from "$lib/state/space.svelte" +import type { Provider, Item, ComplexItem } from "$lib/types/api" export const providerApi = { providerTypes: () => api.get('/provider/providerTypes'), - getProviders: () => api.get(`/provider/${space.id}`), - getProvider: (id: string) => api.get(`/provider/${space.id}/${id}`), - createProvider: (data: Partial) => api.post(`/provider/${space.id}`, data), - updateProvider: (data: Partial) => api.put(`/provider/${space.id}`, data), - deleteProvider: (id: string) => api.delete(`/provider/${space.id}/${id}`), - getAvailableModels: (data: Partial) => api.post(`/provider/${space.id}/avalable-models`, data), + getProviders: (space: string) => api.get(`/provider/${space}`), + getProvider: (space: string,id: string) => api.get(`/provider/${space}/${id}`), + createProvider: (space: string,data: Partial) => api.post(`/provider/${space}`, data), + updateProvider: (space: string,data: Partial) => api.put(`/provider/${space}`, data), + deleteProvider: (space: string,id: string) => api.delete(`/provider/${space}/${id}`), + getAvailableModels: (space: string,data: Partial) => api.post(`/provider/${space}/avalable-models`, data), + getProvidersModels: (space: string,) => api.get(`/provider/${space}/models`), }; diff --git a/backend/web/src/lib/components/Sidebar.svelte b/backend/web/src/lib/components/Sidebar.svelte index 883d7eb..d006bf4 100644 --- a/backend/web/src/lib/components/Sidebar.svelte +++ b/backend/web/src/lib/components/Sidebar.svelte @@ -5,7 +5,7 @@ import logo from "$lib/assets/logo-txt.png" import { space } from '$lib/state/space.svelte' - let {handleMenuDesktop, handleMenuMobile} = $props(); + let {spaceid, handleMenuDesktop, handleMenuMobile} = $props(); const handleLogout = () => { logout() @@ -38,11 +38,11 @@ {/if}
diff --git a/backend/web/src/lib/state/space.svelte.ts b/backend/web/src/lib/state/space.svelte.ts index 78275d4..f0cc494 100644 --- a/backend/web/src/lib/state/space.svelte.ts +++ b/backend/web/src/lib/state/space.svelte.ts @@ -1,6 +1,5 @@ import type { Team } from "$lib/types/api" export const space = $state({ - spaces: [] as Team[], - id: "", + spaces: [] as Team[] }); diff --git a/backend/web/src/lib/types/api.ts b/backend/web/src/lib/types/api.ts index 9a3134d..e792b12 100644 --- a/backend/web/src/lib/types/api.ts +++ b/backend/web/src/lib/types/api.ts @@ -1,6 +1,12 @@ export type Item = { - id: string - text: string + value: string + label: string +} + +export type ComplexItem = { + value: string + label: string + group: string } export interface User { @@ -55,12 +61,18 @@ export interface Provider { _version: string } -export interface Model { +export interface Agent { id: string teamId: string - providerId: string - modelName: string - configuration: object + name: string + systemPrompt: string + defaultProviderId: string + defaultModelId: string + toolsPolicy: string + subAgents: boolean + loopStrategy: string + maxIterations: string + stoppingfCriteria: string _date_created: string _date_updated: string _version: string diff --git a/backend/web/src/routes/(authenticated)/+layout.svelte b/backend/web/src/routes/(authenticated)/+layout.svelte index 63d3b0b..614fc0a 100644 --- a/backend/web/src/routes/(authenticated)/+layout.svelte +++ b/backend/web/src/routes/(authenticated)/+layout.svelte @@ -42,7 +42,6 @@ if (data.me) { auth.user = data.me.user space.spaces = data.me.teams - space.id = data.me.teams[0].id themeToLocalStorage(auth.user.theme) applyTheme() auth.status = 'authenticated' @@ -63,7 +62,7 @@
diff --git a/backend/web/src/routes/(authenticated)/+layout.ts b/backend/web/src/routes/(authenticated)/+layout.ts index fba6264..2ea7f22 100644 --- a/backend/web/src/routes/(authenticated)/+layout.ts +++ b/backend/web/src/routes/(authenticated)/+layout.ts @@ -1,8 +1,9 @@ import { userApi } from "$lib/api/endpoints/user" import type { LayoutLoad } from './$types'; -export const load: LayoutLoad = async () => { +export const load: LayoutLoad = async ({params}) => { return { + spaceid: params.space, me: await userApi.me() } } diff --git a/backend/web/src/routes/(authenticated)/[space]/settings/agents/+page.svelte b/backend/web/src/routes/(authenticated)/[space]/settings/agents/+page.svelte index b746249..7ae3ae5 100644 --- a/backend/web/src/routes/(authenticated)/[space]/settings/agents/+page.svelte +++ b/backend/web/src/routes/(authenticated)/[space]/settings/agents/+page.svelte @@ -1 +1,144 @@ -
Modèles
+ + +
+ + {#if addingAgent} + {#key "new"} + + {/key} + {:else} +
+ + Ajouter un agent +
+ {/if} + + {#each agents as agent (agent.id) } + {#if editingAgent===agent.id} + {#key agent.id} + + {/key} + {:else} +
+
+ { agent.name } +
+ + +
+ {/if} + {/each} + +
diff --git a/backend/web/src/routes/(authenticated)/[space]/settings/agents/+page.ts b/backend/web/src/routes/(authenticated)/[space]/settings/agents/+page.ts index 5536862..f77a43f 100644 --- a/backend/web/src/routes/(authenticated)/[space]/settings/agents/+page.ts +++ b/backend/web/src/routes/(authenticated)/[space]/settings/agents/+page.ts @@ -1,7 +1,14 @@ -export function load() { +import { agentApi } from "$lib/api/endpoints/agent" +import {providerApi} from "$lib/api/endpoints/provider" +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ params }) => { return { title: 'Paramétrage : Agents', - back: "/settings", - theme: "settings" + back: "../settings", + theme: "settings", + space: params.space, + agents: await agentApi.getAgents(params.space), + providersModels: await providerApi.getProvidersModels(params.space), }; } diff --git a/backend/web/src/routes/(authenticated)/[space]/settings/agents/AgentForm.svelte b/backend/web/src/routes/(authenticated)/[space]/settings/agents/AgentForm.svelte new file mode 100644 index 0000000..e4b0bb6 --- /dev/null +++ b/backend/web/src/routes/(authenticated)/[space]/settings/agents/AgentForm.svelte @@ -0,0 +1,88 @@ + + +
+
+
{errors.clear()}}> +
+ + + +
+ +
+ + + +
+ +
+ + @@ -114,7 +114,7 @@ const handleModelChange = () => {
- + diff --git a/backend/web/src/routes/(authenticated)/profile/+page.svelte b/backend/web/src/routes/(authenticated)/profile/+page.svelte index db7b6cb..6cf9b16 100644 --- a/backend/web/src/routes/(authenticated)/profile/+page.svelte +++ b/backend/web/src/routes/(authenticated)/profile/+page.svelte @@ -21,7 +21,7 @@ onMount(async ()=> { addresses = await userApi.listAddresses(form.id) - timezones = (await miscApi.listTimezones()).map((it)=>{return {id:it, text:it}}) + timezones = (await miscApi.listTimezones()).map((it)=>{return {value:it, label:it}}) }) $effect(()=>{ @@ -118,7 +118,7 @@
-
diff --git a/backend/web/src/routes/layout.css b/backend/web/src/routes/layout.css index 0184019..ef3af62 100644 --- a/backend/web/src/routes/layout.css +++ b/backend/web/src/routes/layout.css @@ -199,11 +199,11 @@ h2 { } .svelte-select-list { - @apply absolute right-0 left-0 z-10 max-h-64 w-full overflow-y-auto rounded-sm border-none bg-stone-50 px-4 shadow-md dark:bg-stone-800; + @apply absolute right-0 left-0 z-10 max-h-64 w-full overflow-y-auto rounded-sm border-none bg-stone-50 shadow-md dark:bg-stone-800; } .svelte-select-list .list-group-title { - @apply h-8 cursor-default px-5 text-sm leading-10 font-medium text-ellipsis whitespace-nowrap text-gray-400 uppercase; + @apply h-8 cursor-default px-2 text-sm leading-10 font-medium text-ellipsis whitespace-nowrap text-gray-400 uppercase; } .svelte-select-list .empty { @@ -215,7 +215,7 @@ h2 { } .item.group-item { - @apply px-10; + @apply px-4; } .item.active { diff --git a/go.mod b/go.mod index 73a5680..e2e9e5f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module trankilou.fr/lassistanoque go 1.26.5 require ( - gitea.trankilou.fr/fabien/lasebuche v0.0.0-20260808210632-98bd52d7c29e github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-migrate/migrate/v4 v4.19.1 github.com/joho/godotenv v1.5.1 diff --git a/go.sum b/go.sum index bc230d8..eb71df7 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -gitea.trankilou.fr/fabien/lasebuche v0.0.0-20260808210632-98bd52d7c29e h1:M73kAt45VlJCVwbyFXrBn9oTZRPcoImlUMm0Qj96Y2U= -gitea.trankilou.fr/fabien/lasebuche v0.0.0-20260808210632-98bd52d7c29e/go.mod h1:/Vk2CqBfE7O8jGSQ8H0tB7CI0paO0DAveK2WIgi1FIg= github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY= github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= @@ -11,8 +9,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= @@ -38,22 +34,16 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= -github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo= github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= github.com/mozilla-ai/any-llm-go v0.9.0 h1:+pRG33idm22qcDTyaE97IqX9EGpWocfOUT6QIocnWjI= github.com/mozilla-ai/any-llm-go v0.9.0/go.mod h1:L86RiIeF62s1SzHbMr+tKUj9lZ7AgOPJTxpjveCZMnM= -github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= -github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ollama/ollama v0.17.6 h1:zxInCopQToAMm+OniZSiHFcry03kiL6i1mmcTvpK4Us= github.com/ollama/ollama v0.17.6/go.mod h1:tCX4IMV8DHjl3zY0THxuEkpWDZSOchJpzTuLACpMwFw= github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0= github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -107,13 +97,5 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k= -modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co= -modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= -modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= -modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0= -modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ= turso.tech/database/tursogo v0.7.2 h1:WhsLl67w/Kw7ACEsJaTlXIb1parIgRqUcB8AbNZsBqU= turso.tech/database/tursogo v0.7.2/go.mod h1:tmC+H2Ot+guA9bs5oOcbbmpj1mHFTa/g7QREXG5QHq0=