correction des pb de space : le spaceId est maintenant dans l'url uniquement
This commit is contained in:
1 parent
c52460bac0
commit
0bb8d1cb46
41 files changed
+1460
-118
No files matched your search
@@ -11,6 +11,7 @@ import (
|
|||||||
"trankilou.fr/lassistanoque/backend/internal/adapter/llm"
|
"trankilou.fr/lassistanoque/backend/internal/adapter/llm"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/adapter/security"
|
"trankilou.fr/lassistanoque/backend/internal/adapter/security"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/http"
|
"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/auth"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/service/provider"
|
"trankilou.fr/lassistanoque/backend/internal/service/provider"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/service/storage"
|
"trankilou.fr/lassistanoque/backend/internal/service/storage"
|
||||||
@@ -71,6 +72,7 @@ func runServe() {
|
|||||||
userService := user.NewService(db.UserRepository())
|
userService := user.NewService(db.UserRepository())
|
||||||
storageService := storage.NewService(store)
|
storageService := storage.NewService(store)
|
||||||
providerService := provider.NewService(db.ProviderRepository(), db.UserRepository(), llmEngine)
|
providerService := provider.NewService(db.ProviderRepository(), db.UserRepository(), llmEngine)
|
||||||
|
agentService := agent.NewService(db.AgentRepository(), db.UserRepository())
|
||||||
|
|
||||||
// http server
|
// http server
|
||||||
router := http.NewRouter(http.Dependencies{
|
router := http.NewRouter(http.Dependencies{
|
||||||
@@ -79,6 +81,7 @@ func runServe() {
|
|||||||
UserService: userService,
|
UserService: userService,
|
||||||
TokenManager: tokenManager,
|
TokenManager: tokenManager,
|
||||||
ProviderService: providerService,
|
ProviderService: providerService,
|
||||||
|
AgentService: agentService,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := router.Start(); err != nil {
|
if err := router.Start(); err != nil {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Database interface {
|
|||||||
UserRepository() domain.UserRepository
|
UserRepository() domain.UserRepository
|
||||||
FileRepository() domain.FileRepository
|
FileRepository() domain.FileRepository
|
||||||
ProviderRepository() domain.ProviderRepository
|
ProviderRepository() domain.ProviderRepository
|
||||||
|
AgentRepository() domain.AgentRepository
|
||||||
Migrate() error
|
Migrate() error
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -69,5 +69,9 @@ func (db *TursoDB) FileRepository() domain.FileRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (db *TursoDB) ProviderRepository() domain.ProviderRepository {
|
func (db *TursoDB) ProviderRepository() domain.ProviderRepository {
|
||||||
return NewTursoModelRepository(db.DB)
|
return NewTursoProviderRepository(db.DB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *TursoDB) AgentRepository() domain.AgentRepository {
|
||||||
|
return NewTursoAgentRepository(db.DB)
|
||||||
}
|
}
|
||||||
@@ -6,19 +6,19 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"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/domain"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/utility"
|
"trankilou.fr/lassistanoque/backend/internal/utility"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TursoFileRepository struct {
|
type TursoFileRepository struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
FileTable lasebuche.Table[domain.File]
|
FileTable orm.Table[domain.File]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTursoFileRepository(db *sql.DB) *TursoFileRepository {
|
func NewTursoFileRepository(db *sql.DB) *TursoFileRepository {
|
||||||
dialect := lasebuche.NewSqliteDialect()
|
dialect := orm.NewSqliteDialect()
|
||||||
fileTable, err := lasebuche.NewTable[domain.File](db, dialect, "settings")
|
fileTable, err := orm.NewTable[domain.File](db, dialect, "settings")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("error creating lasebuche team table")
|
log.Fatalf("error creating lasebuche team table")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ drop table tools;
|
|||||||
drop table tasks;
|
drop table tasks;
|
||||||
drop table history;
|
drop table history;
|
||||||
drop table files;
|
drop table files;
|
||||||
|
drop table agents;
|
||||||
@@ -141,3 +141,24 @@ create table files (
|
|||||||
_date_updated numeric ,
|
_date_updated numeric ,
|
||||||
_version text not null
|
_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
|
||||||
|
);
|
||||||
@@ -4,37 +4,40 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"gitea.trankilou.fr/fabien/lasebuche"
|
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TursoModelRepository struct {
|
type TursoProviderRepository struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
providerTable lasebuche.Table[domain.Provider]
|
providerTable orm.Table[domain.Provider]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTursoModelRepository(db *sql.DB) *TursoModelRepository {
|
func NewTursoProviderRepository(db *sql.DB) *TursoProviderRepository {
|
||||||
dialect := lasebuche.NewSqliteDialect()
|
dialect := orm.NewSqliteDialect()
|
||||||
providerTable, err := lasebuche.NewTable[domain.Provider](db, dialect, "providers")
|
providerTable, err := orm.NewTable[domain.Provider](db, dialect, "providers")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("error creating lasebuche team table")
|
log.Fatalf("error creating lasebuche provider table")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &TursoModelRepository{
|
return &TursoProviderRepository{
|
||||||
db: db,
|
db: db,
|
||||||
providerTable: providerTable,
|
providerTable: providerTable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *TursoModelRepository) ListProviders(userID string, teamID string) ([]*domain.Provider, error) {
|
func (r *TursoProviderRepository) ListProviders(userID string, teamID string) ([]*domain.Provider, error) {
|
||||||
return r.providerTable.SelectWhere(
|
return r.providerTable.Select(
|
||||||
|
orm.WithWhere(
|
||||||
"team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)",
|
"team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)",
|
||||||
teamID,
|
teamID,
|
||||||
userID,
|
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(
|
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=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
|
||||||
id,
|
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)
|
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)
|
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(
|
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=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"gitea.trankilou.fr/fabien/lasebuche"
|
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TursoSettingsRepository struct {
|
type TursoSettingsRepository struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
SettingsTable lasebuche.Table[domain.Settings]
|
SettingsTable orm.Table[domain.Settings]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTursoSettingsRepository(db *sql.DB) *TursoSettingsRepository {
|
func NewTursoSettingsRepository(db *sql.DB) *TursoSettingsRepository {
|
||||||
dialect := lasebuche.NewSqliteDialect()
|
dialect := orm.NewSqliteDialect()
|
||||||
settingsTable, err := lasebuche.NewTable[domain.Settings](db, dialect, "settings")
|
settingsTable, err := orm.NewTable[domain.Settings](db, dialect, "settings")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("error creating lasebuche team table")
|
log.Fatalf("error creating lasebuche team table")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,34 +4,33 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||||
|
|
||||||
"gitea.trankilou.fr/fabien/lasebuche"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type TursoUserRepository struct {
|
type TursoUserRepository struct {
|
||||||
DB *sql.DB
|
DB *sql.DB
|
||||||
UserTable lasebuche.Table[domain.User]
|
UserTable orm.Table[domain.User]
|
||||||
TeamTable lasebuche.Table[domain.Team]
|
TeamTable orm.Table[domain.Team]
|
||||||
UserTeamTable lasebuche.Table[domain.UserTeam]
|
UserTeamTable orm.Table[domain.UserTeam]
|
||||||
AddressTable lasebuche.Table[domain.UserAddress]
|
AddressTable orm.Table[domain.UserAddress]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTursoUserRepository(db *sql.DB) *TursoUserRepository {
|
func NewTursoUserRepository(db *sql.DB) *TursoUserRepository {
|
||||||
dialect := lasebuche.NewSqliteDialect()
|
dialect := orm.NewSqliteDialect()
|
||||||
userTable, err := lasebuche.NewTable[domain.User](db, dialect, "users")
|
userTable, err := orm.NewTable[domain.User](db, dialect, "users")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("error creating lasebuche user table")
|
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 {
|
if err != nil {
|
||||||
log.Fatalf("error creating lasebuche team table")
|
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 {
|
if err != nil {
|
||||||
log.Fatalf("error creating lasebuche team table")
|
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 {
|
if err != nil {
|
||||||
log.Fatalf("error creating lasebuche team table")
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
func (ur *TursoUserRepository) GetUserAddress(addrID string) (*domain.UserAddress, error) {
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var providerTypes = []domain.Item{
|
var providerTypes = []domain.Item{
|
||||||
{ID: "anthropic", Text: "Anthropic"},
|
{Value: "anthropic", Label: "Anthropic"},
|
||||||
{ID: "openai", Text: "OpenAI"},
|
{Value: "openai", Label: "OpenAI"},
|
||||||
{ID: "ollama", Text: "Ollama"},
|
{Value: "ollama", Label: "Ollama"},
|
||||||
{ID: "openaicomp", Text: "OpenAI compatible"},
|
{Value: "openaicomp", Label: "OpenAI compatible"},
|
||||||
{ID: "openrouter", Text: "Openrouter"},
|
{Value: "openrouter", Label: "Openrouter"},
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnyLLMEngine struct {
|
type AnyLLMEngine struct {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
type Item struct {
|
type Item struct {
|
||||||
ID string `json:"id"`
|
Value string `json:"value"`
|
||||||
Text string `json:"text"`
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComplexItem struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Group string `json:"group"`
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ func NewModelGroup(prefix string, e *echo.Group, service *provider.Service, midd
|
|||||||
|
|
||||||
model.GET("/providerTypes", modelHandler.ListProviderTypes)
|
model.GET("/providerTypes", modelHandler.ListProviderTypes)
|
||||||
model.GET("/:space", modelHandler.ListProviders)
|
model.GET("/:space", modelHandler.ListProviders)
|
||||||
|
model.GET("/:space/models", modelHandler.ListProvidersModels)
|
||||||
model.POST("/:space/avalable-models", modelHandler.ListAvailableModels)
|
model.POST("/:space/avalable-models", modelHandler.ListAvailableModels)
|
||||||
model.GET("/:space/:provider", modelHandler.GetProvider)
|
model.GET("/:space/:provider", modelHandler.GetProvider)
|
||||||
model.PUT("/:space", modelHandler.UpdateProvider)
|
model.PUT("/:space", modelHandler.UpdateProvider)
|
||||||
@@ -116,8 +117,19 @@ func (h *ModelHandler) ListAvailableModels(c *echo.Context) error {
|
|||||||
provider.TeamID = teamID
|
provider.TeamID = teamID
|
||||||
list, err := h.providerService.ListAvailableModels(c.Request().Context(), &provider)
|
list, err := h.providerService.ListAvailableModels(c.Request().Context(), &provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logger().Error("error listing models: %s", err)
|
c.Logger().Error("error listing available models: %s", err)
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "error creating provider")
|
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)
|
return c.JSON(http.StatusOK, list)
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/labstack/echo/v5/middleware"
|
"github.com/labstack/echo/v5/middleware"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/config"
|
"trankilou.fr/lassistanoque/backend/internal/config"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/http/handlers"
|
"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/auth"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/service/provider"
|
"trankilou.fr/lassistanoque/backend/internal/service/provider"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/service/storage"
|
"trankilou.fr/lassistanoque/backend/internal/service/storage"
|
||||||
@@ -37,6 +38,7 @@ type Dependencies struct {
|
|||||||
AuthService *auth.Service
|
AuthService *auth.Service
|
||||||
UserService *user.Service
|
UserService *user.Service
|
||||||
ProviderService *provider.Service
|
ProviderService *provider.Service
|
||||||
|
AgentService *agent.Service
|
||||||
TokenManager auth.TokenManager
|
TokenManager auth.TokenManager
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +76,7 @@ func NewRouter(deps Dependencies) *Router {
|
|||||||
_ = handlers.NewUserGroup("/user", api, deps.UserService, deps.TokenManager.TokenMiddleware)
|
_ = handlers.NewUserGroup("/user", api, deps.UserService, deps.TokenManager.TokenMiddleware)
|
||||||
_ = handlers.NewMiscGroup("/misc", api, deps.TokenManager.TokenMiddleware)
|
_ = handlers.NewMiscGroup("/misc", api, deps.TokenManager.TokenMiddleware)
|
||||||
_ = handlers.NewModelGroup("/provider", api, deps.ProviderService, deps.TokenManager.TokenMiddleware)
|
_ = handlers.NewModelGroup("/provider", api, deps.ProviderService, deps.TokenManager.TokenMiddleware)
|
||||||
|
_ = handlers.NewAgentGroup("/agent", api, deps.AgentService, deps.TokenManager.TokenMiddleware)
|
||||||
|
|
||||||
return &Router{
|
return &Router{
|
||||||
echo: e,
|
echo: e,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package provider
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
"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)
|
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) {
|
func (s *Service) GetProvider(userID string, teamID string, id string) (*domain.Provider, error) {
|
||||||
return s.repo.GetProvider(userID, teamID, id)
|
return s.repo.GetProvider(userID, teamID, id)
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
|||||||
|
import { api } from '../client';
|
||||||
|
import type { Agent } from "$lib/types/api"
|
||||||
|
|
||||||
|
export const agentApi = {
|
||||||
|
getAgents: (space: string) => api.get<Agent[]>(`/agent/${space}`),
|
||||||
|
getAgent: (space: string, id: string) => api.get<Agent>(`/agent/${space}/${id}`),
|
||||||
|
createAgent: (space: string,data: Partial<Agent>) => api.post<Agent>(`/agent/${space}`, data),
|
||||||
|
updateAgent: (space: string,data: Partial<Agent>) => api.put<Agent>(`/agent/${space}`, data),
|
||||||
|
deleteAgent: (space: string,id: string) => api.delete(`/agent/${space}/${id}`),
|
||||||
|
};
|
||||||
@@ -25,9 +25,4 @@ export const authApi = {
|
|||||||
register: (data: RegistrationData) => api.post<User>('/auth/register', data),
|
register: (data: RegistrationData) => api.post<User>('/auth/register', data),
|
||||||
status: () => api.get<User>('/auth/status'),
|
status: () => api.get<User>('/auth/status'),
|
||||||
login: (data: Credentials) => api.post<UserReponse>('/auth/login', data),
|
login: (data: Credentials) => api.post<UserReponse>('/auth/login', data),
|
||||||
// getAll: () => api.get<User[]>('/users'),
|
|
||||||
// getById: (id: string) => api.get<User>(`/users/${id}`),
|
|
||||||
// create: (data: Partial<User>) => api.post<User>('/users', data),
|
|
||||||
// update: (id: string, data: Partial<User>) => api.put<User>(`/users/${id}`, data),
|
|
||||||
// remove: (id: string) => api.delete<void>(`/users/${id}`),
|
|
||||||
};
|
};
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { api } from '../client';
|
import { api } from '../client';
|
||||||
import type { Provider, Item } from "$lib/types/api"
|
import type { Provider, Item, ComplexItem } from "$lib/types/api"
|
||||||
import { space } from "$lib/state/space.svelte"
|
|
||||||
|
|
||||||
export const providerApi = {
|
export const providerApi = {
|
||||||
providerTypes: () => api.get<Item[]>('/provider/providerTypes'),
|
providerTypes: () => api.get<Item[]>('/provider/providerTypes'),
|
||||||
getProviders: () => api.get<Provider[]>(`/provider/${space.id}`),
|
getProviders: (space: string) => api.get<Provider[]>(`/provider/${space}`),
|
||||||
getProvider: (id: string) => api.get<Provider>(`/provider/${space.id}/${id}`),
|
getProvider: (space: string,id: string) => api.get<Provider>(`/provider/${space}/${id}`),
|
||||||
createProvider: (data: Partial<Provider>) => api.post<Provider>(`/provider/${space.id}`, data),
|
createProvider: (space: string,data: Partial<Provider>) => api.post<Provider>(`/provider/${space}`, data),
|
||||||
updateProvider: (data: Partial<Provider>) => api.put<Provider>(`/provider/${space.id}`, data),
|
updateProvider: (space: string,data: Partial<Provider>) => api.put<Provider>(`/provider/${space}`, data),
|
||||||
deleteProvider: (id: string) => api.delete(`/provider/${space.id}/${id}`),
|
deleteProvider: (space: string,id: string) => api.delete(`/provider/${space}/${id}`),
|
||||||
getAvailableModels: (data: Partial<Provider>) => api.post<string[]>(`/provider/${space.id}/avalable-models`, data),
|
getAvailableModels: (space: string,data: Partial<Provider>) => api.post<string[]>(`/provider/${space}/avalable-models`, data),
|
||||||
|
getProvidersModels: (space: string,) => api.get<ComplexItem[]>(`/provider/${space}/models`),
|
||||||
};
|
};
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
import logo from "$lib/assets/logo-txt.png"
|
import logo from "$lib/assets/logo-txt.png"
|
||||||
import { space } from '$lib/state/space.svelte'
|
import { space } from '$lib/state/space.svelte'
|
||||||
|
|
||||||
let {handleMenuDesktop, handleMenuMobile} = $props();
|
let {spaceid, handleMenuDesktop, handleMenuMobile} = $props();
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
logout()
|
logout()
|
||||||
@@ -38,11 +38,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div id="menu" class="flex-1 overflow-y-scroll">
|
<div id="menu" class="flex-1 overflow-y-scroll">
|
||||||
<Menu label="Nouveau chat" icon="icon-plus" link={`/${space.id}`}/>
|
<Menu label="Nouveau chat" icon="icon-plus" link={`/${spaceid}`}/>
|
||||||
<Menu label="Conversations" icon="icon-chat" link={`/${space.id}/history`}/>
|
<Menu label="Conversations" icon="icon-chat" link={`/${spaceid}/history`}/>
|
||||||
<Menu label="Planification" icon="icon-tasks" link={`/${space.id}/tasks`}/>
|
<Menu label="Planification" icon="icon-tasks" link={`/${spaceid}/tasks`}/>
|
||||||
<Menu label="Connaissances" icon="icon-graduation-cap" link={`/${space.id}/kb`}/>
|
<Menu label="Connaissances" icon="icon-graduation-cap" link={`/${spaceid}/kb`}/>
|
||||||
<Menu label="Réglages" icon="icon-cog" link={`/${space.id}/settings`}/>
|
<Menu label="Réglages" icon="icon-cog" link={`/${spaceid}/settings`}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="profile" class="w-full items-center border-t border-stone-200 dark:border-stone-800">
|
<div id="profile" class="w-full items-center border-t border-stone-200 dark:border-stone-800">
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Team } from "$lib/types/api"
|
import type { Team } from "$lib/types/api"
|
||||||
|
|
||||||
export const space = $state({
|
export const space = $state({
|
||||||
spaces: [] as Team[],
|
spaces: [] as Team[]
|
||||||
id: "",
|
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
export type Item = {
|
export type Item = {
|
||||||
id: string
|
value: string
|
||||||
text: string
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexItem = {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
group: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
@@ -55,12 +61,18 @@ export interface Provider {
|
|||||||
_version: string
|
_version: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Model {
|
export interface Agent {
|
||||||
id: string
|
id: string
|
||||||
teamId: string
|
teamId: string
|
||||||
providerId: string
|
name: string
|
||||||
modelName: string
|
systemPrompt: string
|
||||||
configuration: object
|
defaultProviderId: string
|
||||||
|
defaultModelId: string
|
||||||
|
toolsPolicy: string
|
||||||
|
subAgents: boolean
|
||||||
|
loopStrategy: string
|
||||||
|
maxIterations: string
|
||||||
|
stoppingfCriteria: string
|
||||||
_date_created: string
|
_date_created: string
|
||||||
_date_updated: string
|
_date_updated: string
|
||||||
_version: string
|
_version: string
|
||||||
|
|||||||
@@ -42,7 +42,6 @@
|
|||||||
if (data.me) {
|
if (data.me) {
|
||||||
auth.user = data.me.user
|
auth.user = data.me.user
|
||||||
space.spaces = data.me.teams
|
space.spaces = data.me.teams
|
||||||
space.id = data.me.teams[0].id
|
|
||||||
themeToLocalStorage(auth.user.theme)
|
themeToLocalStorage(auth.user.theme)
|
||||||
applyTheme()
|
applyTheme()
|
||||||
auth.status = 'authenticated'
|
auth.status = 'authenticated'
|
||||||
@@ -63,7 +62,7 @@
|
|||||||
|
|
||||||
<div id="outer" class={[uiState.sidebarDesktopOpen ? "desktopMenuOpen" : "desktopMenuClosed", uiState.sidebarMobileOpen ? "mobileMenuOpen" : "mobileMenuClosed" ]}>
|
<div id="outer" class={[uiState.sidebarDesktopOpen ? "desktopMenuOpen" : "desktopMenuClosed", uiState.sidebarMobileOpen ? "mobileMenuOpen" : "mobileMenuClosed" ]}>
|
||||||
<div id="sidebar" class="flex flex-col">
|
<div id="sidebar" class="flex flex-col">
|
||||||
<Sidebar {handleMenuDesktop} {handleMenuMobile}/>
|
<Sidebar spaceid={data.spaceid} {handleMenuDesktop} {handleMenuMobile}/>
|
||||||
</div>
|
</div>
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<div class="flex flex-col h-screen ">
|
<div class="flex flex-col h-screen ">
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { userApi } from "$lib/api/endpoints/user"
|
import { userApi } from "$lib/api/endpoints/user"
|
||||||
import type { LayoutLoad } from './$types';
|
import type { LayoutLoad } from './$types';
|
||||||
|
|
||||||
export const load: LayoutLoad = async () => {
|
export const load: LayoutLoad = async ({params}) => {
|
||||||
return {
|
return {
|
||||||
|
spaceid: params.space,
|
||||||
me: await userApi.me()
|
me: await userApi.me()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1 +1,144 @@
|
|||||||
<div>Modèles</div>
|
<script lang="ts">
|
||||||
|
|
||||||
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
|
|
||||||
|
import type { Agent } from '$lib/types/api'
|
||||||
|
import type { PageProps } from './$types';
|
||||||
|
import { agentApi } from "$lib/api/endpoints/agent"
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
//import { validateUrl } from '$lib/utilities/validators'
|
||||||
|
import AgentForm from './AgentForm.svelte'
|
||||||
|
|
||||||
|
let { data }: PageProps = $props();
|
||||||
|
|
||||||
|
let agents = $state([] as Agent[])
|
||||||
|
let addingAgent = $state(false)
|
||||||
|
let editingAgent = $state(null as string|null)
|
||||||
|
let agentForm = $state({} as Agent)
|
||||||
|
let errors = new SvelteMap<string,string>()
|
||||||
|
|
||||||
|
onMount(async ()=>{
|
||||||
|
agents = await agentApi.getAgents(data.space)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleEnterAddAgent = () => {
|
||||||
|
if (editingAgent) return
|
||||||
|
errors.clear()
|
||||||
|
agentForm = {} as Agent
|
||||||
|
addingAgent= true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancelAddAgent = () => {
|
||||||
|
addingAgent= false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmitAddAgent = (a: Agent) => {
|
||||||
|
|
||||||
|
if (!a.name) {
|
||||||
|
errors.set("name","Le nom est obligatoire !")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.size === 0) {
|
||||||
|
agentApi.createAgent(data.space, a)
|
||||||
|
.then((p)=>{
|
||||||
|
agents.push(p)
|
||||||
|
addingAgent= false
|
||||||
|
agentForm = {} as Agent
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancelEditAgent = () => {
|
||||||
|
editingAgent = null
|
||||||
|
agentForm = {} as Agent
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmitEditAgent = (a: Agent) => {
|
||||||
|
|
||||||
|
if (!a.name) {
|
||||||
|
errors.set("name","Le nom est obligatoire !")
|
||||||
|
}
|
||||||
|
if (errors.size === 0) {
|
||||||
|
agentApi.updateAgent(data.space,a)
|
||||||
|
.then((p)=>{
|
||||||
|
agents = agents.map((it: Agent)=>{return p.id===it.id ? p : it})
|
||||||
|
editingAgent = null
|
||||||
|
agentForm = {} as Agent
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditAgent = async (id: string) => {
|
||||||
|
errors.clear()
|
||||||
|
if (addingAgent) return
|
||||||
|
agentApi.getAgent(data.space,id)
|
||||||
|
.then((a)=>{
|
||||||
|
agents = agents.filter((it)=>it.id===a.id ? a : it)
|
||||||
|
editingAgent = id
|
||||||
|
})
|
||||||
|
.catch((err)=> console.log(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteAgent = (id: string) => {
|
||||||
|
agentApi.deleteAgent(data.space,id)
|
||||||
|
.then(() => {
|
||||||
|
agents = agents.filter((it: Agent)=>it.id!==id)
|
||||||
|
})
|
||||||
|
.catch((err) => {console.log(err)})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
{#if addingAgent}
|
||||||
|
{#key "new"}
|
||||||
|
<AgentForm
|
||||||
|
item={agentForm}
|
||||||
|
errors={errors}
|
||||||
|
onSave={handleSubmitAddAgent}
|
||||||
|
onCancel={handleCancelAddAgent}
|
||||||
|
providersModels={data.providersModels}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
{:else}
|
||||||
|
<div class="rounded border border-dashed p-2 border-stone-500 cursor-pointer" onclick={handleEnterAddAgent} tabindex="0">
|
||||||
|
<i class="icon-plus"></i>
|
||||||
|
Ajouter un agent
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each agents as agent (agent.id) }
|
||||||
|
{#if editingAgent===agent.id}
|
||||||
|
{#key agent.id}
|
||||||
|
<AgentForm
|
||||||
|
item={agent}
|
||||||
|
errors={errors}
|
||||||
|
onSave={handleSubmitEditAgent}
|
||||||
|
onCancel={handleCancelEditAgent}
|
||||||
|
providersModels={data.providersModels}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
{:else}
|
||||||
|
<div class="rounded border p-2 my-2 border-stone-500 flex">
|
||||||
|
<div class="flex-1">
|
||||||
|
<span>{ agent.name }</span>
|
||||||
|
</div>
|
||||||
|
<button class="bt small mx-1" onclick={()=>handleEditAgent(agent.id)}>
|
||||||
|
<i class="icon-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button class="bt small mx-1" onclick={()=>handleDeleteAgent(agent.id)}>
|
||||||
|
<i class="icon-trash-empty"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -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 {
|
return {
|
||||||
title: 'Paramétrage : Agents',
|
title: 'Paramétrage : Agents',
|
||||||
back: "/settings",
|
back: "../settings",
|
||||||
theme: "settings"
|
theme: "settings",
|
||||||
|
space: params.space,
|
||||||
|
agents: await agentApi.getAgents(params.space),
|
||||||
|
providersModels: await providerApi.getProvidersModels(params.space),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import FormError from '$lib/components/FormError.svelte';
|
||||||
|
import Select from 'svelte-select/no-styles';
|
||||||
|
import type { Agent, ComplexItem } from "$lib/types/api"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
item: Agent
|
||||||
|
providersModels: ComplexItem[],
|
||||||
|
errors: Map<string,string>
|
||||||
|
onSave: (item: Agent) => void
|
||||||
|
onCancel: () => void
|
||||||
|
}
|
||||||
|
let {
|
||||||
|
item,
|
||||||
|
providersModels,
|
||||||
|
errors,
|
||||||
|
onSave,
|
||||||
|
onCancel,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let model = $state("")
|
||||||
|
|
||||||
|
const modelGroupBy = (item: Record<string, unknown>) => item.group as string;
|
||||||
|
|
||||||
|
const autofocus = (node: HTMLElement) => {
|
||||||
|
node.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = (e: Event) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onSave(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = (e: Event) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="formgroup my-2">
|
||||||
|
<form onsubmit={handleSave} onchange={()=>{errors.clear()}}>
|
||||||
|
<div class="formelement">
|
||||||
|
<label for="name">Nom</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
type="text"
|
||||||
|
bind:value={item.name}
|
||||||
|
class="textinput"
|
||||||
|
placeholder="nom affiché"
|
||||||
|
use:autofocus
|
||||||
|
/>
|
||||||
|
<FormError key="name" errors={errors} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="formelement">
|
||||||
|
<label for="systemPrompt">Prompt système</label>
|
||||||
|
<textarea
|
||||||
|
id="systemPrompt"
|
||||||
|
bind:value={item.systemPrompt}
|
||||||
|
class="textinput"
|
||||||
|
placeholder="prompt système"
|
||||||
|
></textarea>
|
||||||
|
<FormError key="baseUrl" errors={errors} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="formelement">
|
||||||
|
<label for="type" class="block my-1 flex-1">Type</label>
|
||||||
|
<Select items={providersModels}
|
||||||
|
valueMode="id"
|
||||||
|
groupBy={modelGroupBy}
|
||||||
|
bind:value={model}
|
||||||
|
/>
|
||||||
|
<FormError key="type" errors={errors} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="text-right">
|
||||||
|
<span>{item._version}</span>
|
||||||
|
<button type="button" class="bt" onclick={handleCancel}>Annuler</button>
|
||||||
|
<button type="submit" class="bt mx-4">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
|
|
||||||
onMount(async ()=>{
|
onMount(async ()=>{
|
||||||
providers = await providerApi.getProviders()
|
providers = await providerApi.getProviders(data.space)
|
||||||
})
|
})
|
||||||
|
|
||||||
const enterAddProviderHandler = () => {
|
const enterAddProviderHandler = () => {
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
errors.set("baseUrl","Mauvaise URL !")
|
errors.set("baseUrl","Mauvaise URL !")
|
||||||
}
|
}
|
||||||
if (errors.size === 0) {
|
if (errors.size === 0) {
|
||||||
providerApi.createProvider(p)
|
providerApi.createProvider(data.space,p)
|
||||||
.then((p)=>{
|
.then((p)=>{
|
||||||
providers.push(p)
|
providers.push(p)
|
||||||
addingProvider= false
|
addingProvider= false
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
errors.set("baseUrl","Mauvaise URL !")
|
errors.set("baseUrl","Mauvaise URL !")
|
||||||
}
|
}
|
||||||
if (errors.size === 0) {
|
if (errors.size === 0) {
|
||||||
providerApi.updateProvider(p)
|
providerApi.updateProvider(data.space,p)
|
||||||
.then((p)=>{
|
.then((p)=>{
|
||||||
providers = providers.map((it)=>{return p.id===it.id ? p : it})
|
providers = providers.map((it)=>{return p.id===it.id ? p : it})
|
||||||
editingProvider = null
|
editingProvider = null
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
const handleEditProvider = async (id: string) => {
|
const handleEditProvider = async (id: string) => {
|
||||||
errors.clear()
|
errors.clear()
|
||||||
if (addingProvider) return
|
if (addingProvider) return
|
||||||
providerApi.getProvider(id)
|
providerApi.getProvider(data.space,id)
|
||||||
.then((data)=>{
|
.then((data)=>{
|
||||||
providers = providers.map((it)=>it.id===id ? data : it)
|
providers = providers.map((it)=>it.id===id ? data : it)
|
||||||
editingProvider = id
|
editingProvider = id
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteProvider = (id: string) => {
|
const handleDeleteProvider = (id: string) => {
|
||||||
providerApi.deleteProvider(id)
|
providerApi.deleteProvider(data.space,id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
providers = providers.filter((it)=>it.id!==id)
|
providers = providers.filter((it)=>it.id!==id)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { providerApi } from "$lib/api/endpoints/provider"
|
import { providerApi } from "$lib/api/endpoints/provider"
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageLoad = async () => {
|
export const load: PageLoad = async ({params}) => {
|
||||||
return {
|
return {
|
||||||
title: 'Paramétrage : Modèles',
|
title: 'Paramétrage : Modèles',
|
||||||
back: "/settings",
|
back: "../settings",
|
||||||
theme: "settings",
|
theme: "settings",
|
||||||
|
space: params.space,
|
||||||
providerTypes: await providerApi.providerTypes(),
|
providerTypes: await providerApi.providerTypes(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -25,8 +25,8 @@ type Selectable = {
|
|||||||
value: string
|
value: string
|
||||||
selected: boolean
|
selected: boolean
|
||||||
}
|
}
|
||||||
let selectedModels = $state(item.models.split(";"))
|
let selectedModels = $state(item.models.split("|"))
|
||||||
let availableModels : Selectable[]= $state(item.models.split(";").map((it)=> {return {value:it, selected:true}}))
|
let availableModels : Selectable[]= $state(item.models.split("|").map((it)=> {return {value:it, selected:true}}))
|
||||||
let modelsErrors = new SvelteMap<string,string>()
|
let modelsErrors = new SvelteMap<string,string>()
|
||||||
|
|
||||||
const autofocus = (node: HTMLElement) => {
|
const autofocus = (node: HTMLElement) => {
|
||||||
@@ -35,7 +35,7 @@ const autofocus = (node: HTMLElement) => {
|
|||||||
|
|
||||||
const handleSave = (e: Event) => {
|
const handleSave = (e: Event) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
item.models = selectedModels.join(";")
|
item.models = selectedModels.join("|")
|
||||||
onSave(item)
|
onSave(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ const handleCancel = (e: Event) => {
|
|||||||
const fetchModels = (e: Event) => {
|
const fetchModels = (e: Event) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
modelsErrors.clear()
|
modelsErrors.clear()
|
||||||
providerApi.getAvailableModels(item)
|
providerApi.getAvailableModels(item.teamId, item)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
availableModels = data.map((it)=>{
|
availableModels = data.map((it)=>{
|
||||||
return { value: it, selected: selectedModels.includes(it)}
|
return { value: it, selected: selectedModels.includes(it)}
|
||||||
@@ -81,7 +81,7 @@ const handleModelChange = () => {
|
|||||||
<div class="formelement">
|
<div class="formelement">
|
||||||
<label for="type" class="block my-1 flex-1">Type</label>
|
<label for="type" class="block my-1 flex-1">Type</label>
|
||||||
<Select items={providerTypes}
|
<Select items={providerTypes}
|
||||||
valueMode="id" itemId="id" label="text"
|
valueMode="id"
|
||||||
bind:value={item.type}
|
bind:value={item.type}
|
||||||
/>
|
/>
|
||||||
<FormError key="type" errors={errors} />
|
<FormError key="type" errors={errors} />
|
||||||
@@ -114,7 +114,7 @@ const handleModelChange = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="formelement">
|
<div class="formelement">
|
||||||
<label for="apiKey">Models</label>
|
<label for="models">Models</label>
|
||||||
<button class="bt small" onclick={fetchModels}>
|
<button class="bt small" onclick={fetchModels}>
|
||||||
Afficher les modèles disponibles
|
Afficher les modèles disponibles
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
onMount(async ()=> {
|
onMount(async ()=> {
|
||||||
addresses = await userApi.listAddresses(form.id)
|
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(()=>{
|
$effect(()=>{
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
|
|
||||||
<div class="formelement">
|
<div class="formelement">
|
||||||
<label for="timezone" class="block my-1 flex-1">Timezone</label>
|
<label for="timezone" class="block my-1 flex-1">Timezone</label>
|
||||||
<Select items={timezones} bind:value={form.timezone} valueMode="id" itemId="id" label="text"/>
|
<Select items={timezones} bind:value={form.timezone} valueMode="id" />
|
||||||
<FormError key="timezone" errors={errors} />
|
<FormError key="timezone" errors={errors} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -199,11 +199,11 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.svelte-select-list {
|
.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 {
|
.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 {
|
.svelte-select-list .empty {
|
||||||
@@ -215,7 +215,7 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item.group-item {
|
.item.group-item {
|
||||||
@apply px-10;
|
@apply px-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item.active {
|
.item.active {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ module trankilou.fr/lassistanoque
|
|||||||
go 1.26.5
|
go 1.26.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.trankilou.fr/fabien/lasebuche v0.0.0-20260808210632-98bd52d7c29e
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
|||||||
@@ -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 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY=
|
||||||
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
|
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=
|
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/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 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
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 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
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=
|
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/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 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
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 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
|
||||||
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
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 h1:+pRG33idm22qcDTyaE97IqX9EGpWocfOUT6QIocnWjI=
|
||||||
github.com/mozilla-ai/any-llm-go v0.9.0/go.mod h1:L86RiIeF62s1SzHbMr+tKUj9lZ7AgOPJTxpjveCZMnM=
|
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 h1:zxInCopQToAMm+OniZSiHFcry03kiL6i1mmcTvpK4Us=
|
||||||
github.com/ollama/ollama v0.17.6/go.mod h1:tCX4IMV8DHjl3zY0THxuEkpWDZSOchJpzTuLACpMwFw=
|
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 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0=
|
||||||
github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
|
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 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
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=
|
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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 h1:WhsLl67w/Kw7ACEsJaTlXIb1parIgRqUcB8AbNZsBqU=
|
||||||
turso.tech/database/tursogo v0.7.2/go.mod h1:tmC+H2Ot+guA9bs5oOcbbmpj1mHFTa/g7QREXG5QHq0=
|
turso.tech/database/tursogo v0.7.2/go.mod h1:tmC+H2Ot+guA9bs5oOcbbmpj1mHFTa/g7QREXG5QHq0=
|
||||||
Reference in new issue
Block a user