Compare commits
2
Commits
0a6ffab050
...
345129c7bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
345129c7bf | ||
|
|
87eeb41364 |
No files matched your search
@@ -1,105 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/auth/password"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/database"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/file"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/llm"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/security"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/weather"
|
||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/auth"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/chat"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/storage"
|
||||
)
|
||||
|
||||
var db database.Database
|
||||
var store storage.StorageProvider
|
||||
var userRepository domain.UserRepository
|
||||
var providerRepository domain.ProviderRepository
|
||||
var agentRepository domain.AgentRepository
|
||||
var chatRepository domain.ChatRepository
|
||||
var settingsRepository domain.SettingsRepository
|
||||
var authenticators = make(map[string]auth.Authenticator)
|
||||
var tokenManager auth.TokenManager
|
||||
var llmengine domain.LLMEngine
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
|
||||
db, err := database.GetDatabase()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = db.Migrate()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// repositories
|
||||
userRepository = db.UserRepository()
|
||||
providerRepository = db.ProviderRepository()
|
||||
agentRepository = db.AgentRepository()
|
||||
chatRepository = db.ChatRepository()
|
||||
settingsRepository = db.SettingsRepository()
|
||||
|
||||
// storage
|
||||
store, err = file.GetStorageProvider(db.FileRepository())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// authentication
|
||||
tokenManager = security.NewJwtTokenManager(12*time.Hour, 7*24*time.Hour, "lassistanoque")
|
||||
pwdAuth := password.NewPasswordAuthenticator(tokenManager, GetUserRepository())
|
||||
authenticators["password"] = pwdAuth
|
||||
|
||||
// llm
|
||||
llmengine = llm.NewAnyLLMEngine()
|
||||
|
||||
// llm tools
|
||||
chat.RegisterTool(&weather.WeatherTool{})
|
||||
}
|
||||
|
||||
func GetStorageProvider() storage.StorageProvider {
|
||||
return store
|
||||
}
|
||||
|
||||
func GetUserRepository() domain.UserRepository {
|
||||
return userRepository
|
||||
}
|
||||
|
||||
func GetProviderRepository() domain.ProviderRepository {
|
||||
return providerRepository
|
||||
}
|
||||
|
||||
func GetAgentRepository() domain.AgentRepository {
|
||||
return agentRepository
|
||||
}
|
||||
|
||||
func GetChatRepository() domain.ChatRepository {
|
||||
return chatRepository
|
||||
}
|
||||
|
||||
func GetSettingsRepository() domain.SettingsRepository {
|
||||
return settingsRepository
|
||||
}
|
||||
|
||||
func GetAuthenticators() map[string]auth.Authenticator {
|
||||
return authenticators
|
||||
}
|
||||
|
||||
func GetTokenManager() auth.TokenManager {
|
||||
return tokenManager
|
||||
}
|
||||
|
||||
func GetLLMEngine() domain.LLMEngine {
|
||||
return llmengine
|
||||
}
|
||||
|
||||
func Close() {
|
||||
db.Close()
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/auth/password"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/database"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/file"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/llm"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/security"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/searxng"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/weather"
|
||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/agent"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/auth"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/chat"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/provider"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/storage"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/tool"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/user"
|
||||
)
|
||||
|
||||
var db database.Database
|
||||
|
||||
// adapters
|
||||
var storeAdapter storage.StorageProvider
|
||||
var llmengine domain.LLMEngine
|
||||
|
||||
// services
|
||||
var authService *auth.Service
|
||||
var userService *user.Service
|
||||
var storageService *storage.Service
|
||||
var providerService *provider.Service
|
||||
var agentService *agent.Service
|
||||
var toolService *tool.Service
|
||||
var chatService *chat.Service
|
||||
var tokenManager auth.TokenManager
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
|
||||
db, err = database.GetDatabase()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = db.Migrate()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Close() {
|
||||
db.Close()
|
||||
}
|
||||
|
||||
// Adapters
|
||||
func getStoreAdapter() storage.StorageProvider {
|
||||
if storeAdapter == nil {
|
||||
var err error
|
||||
storeAdapter, err = file.GetStorageProvider(db.FileRepository())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return storeAdapter
|
||||
}
|
||||
|
||||
func getAuthenticators() map[string]auth.Authenticator {
|
||||
authenticators := make(map[string]auth.Authenticator)
|
||||
pwdAuth := password.NewPasswordAuthenticator(GetTokenManager(), db.UserRepository())
|
||||
authenticators["password"] = pwdAuth
|
||||
return authenticators
|
||||
}
|
||||
|
||||
func GetTokenManager() auth.TokenManager {
|
||||
if tokenManager == nil {
|
||||
tokenManager = security.NewJwtTokenManager(12*time.Hour, 7*24*time.Hour, "lassistanoque")
|
||||
}
|
||||
return tokenManager
|
||||
}
|
||||
|
||||
func GetLLMAdapter() domain.LLMEngine {
|
||||
if llmengine == nil {
|
||||
llmengine = llm.NewAnyLLMEngine()
|
||||
}
|
||||
return llmengine
|
||||
}
|
||||
|
||||
// Services
|
||||
func GetStorageService() *storage.Service {
|
||||
if storageService == nil {
|
||||
storageService = storage.NewService(getStoreAdapter())
|
||||
}
|
||||
return storageService
|
||||
}
|
||||
|
||||
func GetAuthService() *auth.Service {
|
||||
if authService == nil {
|
||||
authService = auth.NewService(db.SettingsRepository(), db.UserRepository(), getAuthenticators())
|
||||
}
|
||||
return authService
|
||||
}
|
||||
|
||||
func GetUserService() *user.Service {
|
||||
if userService == nil {
|
||||
userService = user.NewService(db.UserRepository())
|
||||
}
|
||||
return userService
|
||||
}
|
||||
func GetProviderService() *provider.Service {
|
||||
if providerService == nil {
|
||||
providerService = provider.NewService(db.ProviderRepository(), db.UserRepository(), GetLLMAdapter())
|
||||
}
|
||||
return providerService
|
||||
}
|
||||
|
||||
func GetAgentService() *agent.Service {
|
||||
if agentService == nil {
|
||||
agentService = agent.NewService(db.AgentRepository(), db.UserRepository())
|
||||
}
|
||||
return agentService
|
||||
}
|
||||
func GetChatService() *chat.Service {
|
||||
if chatService == nil {
|
||||
chatService = chat.NewService(
|
||||
db.UserRepository(),
|
||||
db.AgentRepository(),
|
||||
db.ProviderRepository(),
|
||||
db.ChatRepository(),
|
||||
GetLLMAdapter(),
|
||||
GetToolService(),
|
||||
)
|
||||
}
|
||||
return chatService
|
||||
}
|
||||
|
||||
func GetToolService() *tool.Service {
|
||||
if toolService == nil {
|
||||
toolService = tool.NewService(db.ToolRepository(), db.UserRepository())
|
||||
toolService.RegisterToolImpl(&weather.WeatherTool{})
|
||||
toolService.RegisterToolImpl(&searxng.SearxngTool{})
|
||||
}
|
||||
return toolService
|
||||
}
|
||||
+11
-26
@@ -11,14 +11,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"trankilou.fr/lassistanoque/backend/internal/brain"
|
||||
"trankilou.fr/lassistanoque/backend/internal/gateway"
|
||||
"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/chat"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/provider"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/storage"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/user"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -44,35 +38,26 @@ func runServe() {
|
||||
|
||||
defer Close()
|
||||
|
||||
// services
|
||||
authService := auth.NewService(GetSettingsRepository(), GetUserRepository(), GetAuthenticators())
|
||||
userService := user.NewService(GetUserRepository())
|
||||
storageService := storage.NewService(GetStorageProvider())
|
||||
providerService := provider.NewService(GetProviderRepository(), GetUserRepository(), GetLLMEngine())
|
||||
agentService := agent.NewService(GetAgentRepository(), GetUserRepository())
|
||||
chatService := chat.NewService(GetUserRepository(), GetAgentRepository(), GetProviderRepository(), GetChatRepository(), GetLLMEngine())
|
||||
|
||||
// http server
|
||||
httpRouter := http.NewRouter(http.Dependencies{
|
||||
StorageService: storageService,
|
||||
AuthService: authService,
|
||||
UserService: userService,
|
||||
ProviderService: providerService,
|
||||
AgentService: agentService,
|
||||
ChatService: chatService,
|
||||
StorageService: GetStorageService(),
|
||||
AuthService: GetAuthService(),
|
||||
UserService: GetUserService(),
|
||||
ProviderService: GetProviderService(),
|
||||
AgentService: GetAgentService(),
|
||||
ChatService: GetChatService(),
|
||||
ToolService: GetToolService(),
|
||||
TokenManager: GetTokenManager(),
|
||||
})
|
||||
|
||||
brainRouter := brain.NewRouter(brain.Dependencies{
|
||||
ChatRepository: GetChatRepository(),
|
||||
})
|
||||
gatewayRouter := gateway.NewRouter(gateway.Dependencies{})
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
// start Brain router
|
||||
go func() {
|
||||
if err := brainRouter.Start(); err != nil && !errors.Is(err, nethttp.ErrServerClosed) {
|
||||
if err := gatewayRouter.Start(); err != nil && !errors.Is(err, nethttp.ErrServerClosed) {
|
||||
slog.Error("failed to start llm server", "error", err)
|
||||
}
|
||||
}()
|
||||
@@ -97,7 +82,7 @@ func runServe() {
|
||||
// stop LLM service
|
||||
ctx, cancel2 := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel2()
|
||||
if err := brainRouter.Stop(ctx); err != nil {
|
||||
if err := gatewayRouter.Stop(ctx); err != nil {
|
||||
slog.Error("failed to stop llm server", "error", err)
|
||||
}
|
||||
slog.Info("LLM server stopped gracefully")
|
||||
|
||||
@@ -15,6 +15,7 @@ type Database interface {
|
||||
ProviderRepository() domain.ProviderRepository
|
||||
AgentRepository() domain.AgentRepository
|
||||
ChatRepository() domain.ChatRepository
|
||||
ToolRepository() domain.ToolRepository
|
||||
Migrate() error
|
||||
Close()
|
||||
}
|
||||
|
||||
@@ -79,3 +79,7 @@ func (db *TursoDB) AgentRepository() domain.AgentRepository {
|
||||
func (db *TursoDB) ChatRepository() domain.ChatRepository {
|
||||
return NewTursoChatRepository(db.DB)
|
||||
}
|
||||
|
||||
func (db *TursoDB) ToolRepository() domain.ToolRepository {
|
||||
return NewTursoToolRepository(db.DB)
|
||||
}
|
||||
@@ -95,9 +95,8 @@ create table tools (
|
||||
id text not null primary key,
|
||||
team_id text not null,
|
||||
name text not null,
|
||||
type text not null,
|
||||
enabled numeric not null default 1,
|
||||
configuration text not null default '{}',
|
||||
configuration text not null default '',
|
||||
_date_created numeric not null default current_timestamp,
|
||||
_date_updated numeric,
|
||||
_version text not null
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package turso
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
|
||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||
)
|
||||
|
||||
type TursoToolRepository struct {
|
||||
db *sql.DB
|
||||
ToolTable orm.Table[domain.Tool]
|
||||
}
|
||||
|
||||
func NewTursoToolRepository(db *sql.DB) *TursoToolRepository {
|
||||
dialect := orm.NewSqliteDialect()
|
||||
ToolTable, err := orm.NewTable[domain.Tool](db, dialect, "tools")
|
||||
if err != nil {
|
||||
log.Fatalf("error creating lasebuche Tool table")
|
||||
}
|
||||
|
||||
return &TursoToolRepository{
|
||||
db: db,
|
||||
ToolTable: ToolTable,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TursoToolRepository) ListTools(userID string, teamID string) ([]*domain.Tool, error) {
|
||||
return r.ToolTable.Select(
|
||||
orm.WithWhere(
|
||||
"team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)",
|
||||
teamID,
|
||||
userID,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (r *TursoToolRepository) GetToolByName(userID string, teamID string, name string) (*domain.Tool, error) {
|
||||
return r.ToolTable.SelectOne(
|
||||
"name=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
|
||||
name,
|
||||
teamID,
|
||||
userID,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *TursoToolRepository) GetToolByID(userID string, teamID string, id string) (*domain.Tool, error) {
|
||||
return r.ToolTable.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 *TursoToolRepository) CreateTool(userID string, Tool *domain.Tool) (*domain.Tool, error) {
|
||||
return r.ToolTable.Insert(Tool)
|
||||
}
|
||||
|
||||
func (r *TursoToolRepository) UpdateTool(userID string, Tool *domain.Tool) (*domain.Tool, error) {
|
||||
return r.ToolTable.Update(Tool)
|
||||
}
|
||||
|
||||
func (r *TursoToolRepository) DeleteTool(userID string, teamID string, name string) error {
|
||||
return r.ToolTable.DeleteWhere(
|
||||
"name=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
|
||||
name,
|
||||
teamID,
|
||||
userID,
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package searxng
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||
)
|
||||
|
||||
type SearxngConfig struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type SearxngTool struct {
|
||||
}
|
||||
|
||||
func (t *SearxngTool) Name() string {
|
||||
return "searxng"
|
||||
}
|
||||
|
||||
func (t *SearxngTool) Description() string {
|
||||
return "Search the web using a SearXNG instance. Returns a list of relevant results with title, URL, and snippet."
|
||||
}
|
||||
|
||||
func (t *SearxngTool) Params() []domain.ToolParam {
|
||||
return []domain.ToolParam{
|
||||
{JsonName: "url", Type: "string", Mandatory: true, Description: "Searxng URL"},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *SearxngTool) Definition(ctx context.Context) *domain.ToolDefinition {
|
||||
return &domain.ToolDefinition{
|
||||
Type: "function",
|
||||
Function: &domain.ToolFunction{
|
||||
Name: t.Name(),
|
||||
Description: t.Description(),
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"query": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The search query",
|
||||
},
|
||||
"categories": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Comma-separated list of categories: general, news, images, science, it, social_media. Defaults to 'general'.",
|
||||
},
|
||||
"language": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Language code for results, e.g. 'fr', 'en'. Defaults to 'all'.",
|
||||
},
|
||||
},
|
||||
"required": []string{"query"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type searxngResponse struct {
|
||||
Results []searxngResult `json:"results"`
|
||||
}
|
||||
|
||||
type searxngResult struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Content string `json:"content"`
|
||||
Engine string `json:"engine"`
|
||||
}
|
||||
|
||||
func (t *SearxngTool) Execute(ctx context.Context, input []byte, configuration string) ([]byte, error) {
|
||||
|
||||
slog.Debug("[searxng]", "input", string(input))
|
||||
|
||||
var params struct {
|
||||
Query string `json:"query"`
|
||||
Categories string `json:"categories"`
|
||||
Language string `json:"language"`
|
||||
}
|
||||
if err := json.Unmarshal(input, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
|
||||
var toolCfg SearxngConfig
|
||||
err := json.Unmarshal([]byte(configuration), &toolCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("searxng tool not configured: %w", err)
|
||||
}
|
||||
if toolCfg.URL == "" {
|
||||
return nil, fmt.Errorf("searxng tool config missing 'url' field")
|
||||
}
|
||||
baseURL := toolCfg.URL
|
||||
|
||||
categories := "general"
|
||||
if params.Categories != "" {
|
||||
categories = params.Categories
|
||||
}
|
||||
language := "all"
|
||||
if params.Language != "" {
|
||||
language = params.Language
|
||||
}
|
||||
|
||||
reqURL := fmt.Sprintf("%s/search?q=%s&format=json&categories=%s&language=%s",
|
||||
strings.TrimRight(baseURL, "/"),
|
||||
url.QueryEscape(params.Query),
|
||||
url.QueryEscape(categories),
|
||||
url.QueryEscape(language),
|
||||
)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("searxng request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("searxng returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var searchResp searxngResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
if len(searchResp.Results) == 0 {
|
||||
return []byte("No results found."), nil
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "Search results for \"%s\":\n\n", params.Query)
|
||||
for i, r := range searchResp.Results {
|
||||
fmt.Fprintf(&sb, "%d. %s\n", i+1, r.Title)
|
||||
fmt.Fprintf(&sb, " URL: %s\n", r.URL)
|
||||
if r.Content != "" {
|
||||
fmt.Fprintf(&sb, " %s\n", r.Content)
|
||||
}
|
||||
fmt.Fprintf(&sb, "\n")
|
||||
}
|
||||
|
||||
return []byte(sb.String()), nil
|
||||
}
|
||||
@@ -25,6 +25,10 @@ func (t *WeatherTool) Description() string {
|
||||
return "Get the current weather for a given location"
|
||||
}
|
||||
|
||||
func (t *WeatherTool) Params() []domain.ToolParam {
|
||||
return []domain.ToolParam{}
|
||||
}
|
||||
|
||||
func (t *WeatherTool) Definition(ctx context.Context) *domain.ToolDefinition {
|
||||
return &domain.ToolDefinition{
|
||||
Type: "function",
|
||||
@@ -45,7 +49,7 @@ func (t *WeatherTool) Definition(ctx context.Context) *domain.ToolDefinition {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *WeatherTool) Execute(ctx context.Context, input []byte) ([]byte, error) {
|
||||
func (t *WeatherTool) Execute(ctx context.Context, input []byte, configuration string) ([]byte, error) {
|
||||
|
||||
var params struct {
|
||||
Location string `json:"location"`
|
||||
|
||||
@@ -29,6 +29,7 @@ type Config struct {
|
||||
StorageS3AccessKeySecret string // S3
|
||||
StorageS3UseSSL string // S3
|
||||
ServerID string
|
||||
SearXngUrl string
|
||||
}
|
||||
|
||||
var config *Config
|
||||
|
||||
@@ -66,11 +66,19 @@ type ChatParams struct {
|
||||
OnChunk StreamCallback
|
||||
}
|
||||
|
||||
type Tool interface {
|
||||
type ToolParam struct {
|
||||
JsonName string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Mandatory bool `json:"mandatory"`
|
||||
}
|
||||
|
||||
type ITool interface {
|
||||
Name() string
|
||||
Description() string
|
||||
Params() []ToolParam
|
||||
Definition(ctx context.Context) *ToolDefinition
|
||||
Execute(ctx context.Context, input []byte) ([]byte, error)
|
||||
Execute(ctx context.Context, input []byte, configuration string) ([]byte, error)
|
||||
}
|
||||
|
||||
type ToolDefinition struct {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type Tool struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
TeamID string `db:"team_id" json:"teamId"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
Configuration string `db:"configuration" json:"configuration"`
|
||||
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 ToolRepository interface {
|
||||
ListTools(userID string, teamID string) ([]*Tool, error)
|
||||
GetToolByName(userID string, teamID string, name string) (*Tool, error)
|
||||
GetToolByID(userID string, teamID string, id string) (*Tool, error)
|
||||
CreateTool(userID string, agent *Tool) (*Tool, error)
|
||||
UpdateTool(userID string, agent *Tool) (*Tool, error)
|
||||
DeleteTool(userID string, teamID string, name string) error
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package brain
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package brain
|
||||
package gateway
|
||||
|
||||
type Session struct {
|
||||
serverID string
|
||||
@@ -0,0 +1,123 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/auth"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/tool"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/user"
|
||||
)
|
||||
|
||||
func NewToolGroup(
|
||||
prefix string,
|
||||
e *echo.Group,
|
||||
ToolService *tool.Service,
|
||||
userService *user.Service,
|
||||
middlewares ...echo.MiddlewareFunc) *echo.Group {
|
||||
|
||||
ToolHandler := &ToolHandler{
|
||||
ToolService: ToolService,
|
||||
userService: userService,
|
||||
}
|
||||
Tool := e.Group(prefix, middlewares...)
|
||||
|
||||
Tool.GET("/:space", ToolHandler.ListTools)
|
||||
Tool.GET("/:space/:tool", ToolHandler.GetTool)
|
||||
Tool.GET("/:space/:tool/params", ToolHandler.GetToolParams)
|
||||
Tool.PUT("/:space", ToolHandler.UpdateTool)
|
||||
Tool.POST("/:space", ToolHandler.CreateTool)
|
||||
//Tool.DELETE("/:space/:tool", ToolHandler.DeleteTool)
|
||||
|
||||
return Tool
|
||||
}
|
||||
|
||||
type ToolHandler struct {
|
||||
ToolService *tool.Service
|
||||
userService *user.Service
|
||||
}
|
||||
|
||||
func (h *ToolHandler) ListTools(c *echo.Context) error {
|
||||
userID := c.Get(auth.ContextUserIDKey).(string)
|
||||
teamID := c.Param("space")
|
||||
Tools, err := h.ToolService.ListTools(userID, teamID)
|
||||
if err != nil {
|
||||
c.Logger().Error("error listing Tools", "error", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "error listing Tools")
|
||||
}
|
||||
return c.JSON(http.StatusOK, Tools)
|
||||
}
|
||||
|
||||
func (h *ToolHandler) GetTool(c *echo.Context) error {
|
||||
userID := c.Get(auth.ContextUserIDKey).(string)
|
||||
teamID := c.Param("space")
|
||||
ToolID := c.Param("tool")
|
||||
Tool, err := h.ToolService.GetTool(userID, teamID, ToolID)
|
||||
if err != nil {
|
||||
c.Logger().Error("error getting Tool", "error", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "error getting Tool")
|
||||
}
|
||||
return c.JSON(http.StatusOK, Tool)
|
||||
}
|
||||
|
||||
func (h *ToolHandler) GetToolParams(c *echo.Context) error {
|
||||
userID := c.Get(auth.ContextUserIDKey).(string)
|
||||
teamID := c.Param("space")
|
||||
toolID := c.Param("tool")
|
||||
tool, err := h.ToolService.GetTool(userID, teamID, toolID)
|
||||
if err != nil {
|
||||
c.Logger().Error("error getting Tool", "error", err, "teamid", teamID, "toolID", toolID)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "error getting Tool")
|
||||
}
|
||||
toolImpl := h.ToolService.GetToolImpl(tool.Name)
|
||||
return c.JSON(http.StatusOK, toolImpl.Params())
|
||||
}
|
||||
|
||||
func (h *ToolHandler) CreateTool(c *echo.Context) error {
|
||||
userID := c.Get(auth.ContextUserIDKey).(string)
|
||||
teamID := c.Param("space")
|
||||
var Tool domain.Tool
|
||||
err := c.Bind(&Tool)
|
||||
if err != nil {
|
||||
c.Logger().Error("error binding Tool", "error", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "error binding Tool")
|
||||
}
|
||||
Tool.TeamID = teamID
|
||||
updTool, err := h.ToolService.CreateTool(userID, &Tool)
|
||||
if err != nil {
|
||||
c.Logger().Error("error creating Tool", "error", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "error creating Tool")
|
||||
}
|
||||
return c.JSON(http.StatusOK, updTool)
|
||||
}
|
||||
|
||||
func (h *ToolHandler) UpdateTool(c *echo.Context) error {
|
||||
userID := c.Get(auth.ContextUserIDKey).(string)
|
||||
teamID := c.Param("space")
|
||||
var Tool domain.Tool
|
||||
err := c.Bind(&Tool)
|
||||
if err != nil {
|
||||
c.Logger().Error("error binding Tool", "error", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "error binding Tool")
|
||||
}
|
||||
Tool.TeamID = teamID
|
||||
updTool, err := h.ToolService.UpdateTool(userID, &Tool)
|
||||
if err != nil {
|
||||
c.Logger().Error("error updating Tool", "error", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "error updating Tool")
|
||||
}
|
||||
return c.JSON(http.StatusOK, updTool)
|
||||
}
|
||||
|
||||
// func (h *ToolHandler) DeleteTool(c *echo.Context) error {
|
||||
// userID := c.Get(auth.ContextUserIDKey).(string)
|
||||
// teamID := c.Param("space")
|
||||
// ToolID := c.Param("tool")
|
||||
// err := h.ToolService.DeleteTool(userID, teamID, ToolID)
|
||||
// if err != nil {
|
||||
// c.Logger().Error("error creating Tool", "error", err)
|
||||
// return echo.NewHTTPError(http.StatusBadRequest, "error creating Tool")
|
||||
// }
|
||||
// return c.JSON(http.StatusOK, ToolID)
|
||||
// }
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/chat"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/provider"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/storage"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/tool"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/user"
|
||||
)
|
||||
|
||||
@@ -42,6 +43,7 @@ type Dependencies struct {
|
||||
ProviderService *provider.Service
|
||||
AgentService *agent.Service
|
||||
ChatService *chat.Service
|
||||
ToolService *tool.Service
|
||||
TokenManager auth.TokenManager
|
||||
}
|
||||
|
||||
@@ -82,6 +84,7 @@ func NewRouter(deps Dependencies) *Router {
|
||||
_ = handlers.NewMiscGroup("/misc", api, deps.TokenManager.TokenMiddleware)
|
||||
_ = handlers.NewModelGroup("/provider", api, deps.ProviderService, deps.TokenManager.TokenMiddleware)
|
||||
_ = handlers.NewAgentGroup("/agent", api, deps.AgentService, deps.UserService, deps.TokenManager.TokenMiddleware)
|
||||
_ = handlers.NewToolGroup("/tool", api, deps.ToolService, deps.UserService, deps.TokenManager.TokenMiddleware)
|
||||
_ = handlers.NewTeamGroup("/team", api, deps.UserService, deps.TokenManager.TokenMiddleware)
|
||||
_ = handlers.NewChatGroup("/chat", api, deps.ChatService, deps.TokenManager.TokenMiddleware)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/tool"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -17,6 +18,7 @@ type Service struct {
|
||||
repoChat domain.ChatRepository
|
||||
llmEngine domain.LLMEngine
|
||||
runningSessions map[string]*ChatSession
|
||||
toolService *tool.Service
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
@@ -26,6 +28,7 @@ func NewService(
|
||||
repoProvider domain.ProviderRepository,
|
||||
repoChat domain.ChatRepository,
|
||||
llmEngine domain.LLMEngine,
|
||||
toolService *tool.Service,
|
||||
) *Service {
|
||||
return &Service{
|
||||
repoUser: repoUser,
|
||||
@@ -33,6 +36,7 @@ func NewService(
|
||||
repoProvider: repoProvider,
|
||||
repoChat: repoChat,
|
||||
llmEngine: llmEngine,
|
||||
toolService: toolService,
|
||||
runningSessions: make(map[string]*ChatSession),
|
||||
}
|
||||
}
|
||||
@@ -194,6 +198,7 @@ func (s *Service) runQuery(
|
||||
modelID: modelID,
|
||||
messages: messages,
|
||||
llmEngine: s.llmEngine,
|
||||
toolService: s.toolService,
|
||||
subscribers: make([]domain.StreamCallback, 0),
|
||||
repoChat: s.repoChat,
|
||||
}
|
||||
|
||||
@@ -2,11 +2,17 @@ package chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||
"trankilou.fr/lassistanoque/backend/internal/service/tool"
|
||||
)
|
||||
|
||||
//go:embed system_prompt.md
|
||||
var systemSystemPrompt string
|
||||
|
||||
type ChatSession struct {
|
||||
userID string
|
||||
teamID string
|
||||
@@ -16,6 +22,7 @@ type ChatSession struct {
|
||||
modelID string
|
||||
messages []*domain.Message
|
||||
llmEngine domain.LLMEngine
|
||||
toolService *tool.Service
|
||||
repoChat domain.ChatRepository
|
||||
subscribers []domain.StreamCallback
|
||||
mu sync.Mutex
|
||||
@@ -26,7 +33,7 @@ func (s *ChatSession) run(ctx context.Context) error {
|
||||
params := &domain.LLMParams{}
|
||||
|
||||
tools := make([]*domain.ToolDefinition, 0)
|
||||
for _, t := range allTools {
|
||||
for _, t := range s.toolService.GetAllToolImpl() {
|
||||
tools = append(tools, t.Definition(ctx))
|
||||
}
|
||||
params.Tools = tools
|
||||
@@ -43,7 +50,7 @@ func (s *ChatSession) run(ctx context.Context) error {
|
||||
|
||||
systemPrompt := &domain.Message{
|
||||
Role: string(domain.RoleSystem),
|
||||
Content: s.agent.SystemPrompt,
|
||||
Content: systemSystemPrompt + "\n\n## Customization\n\n" + s.agent.SystemPrompt,
|
||||
}
|
||||
|
||||
continue_loop := true
|
||||
@@ -72,8 +79,21 @@ func (s *ChatSession) run(ctx context.Context) error {
|
||||
continue_loop = true
|
||||
for _, tc := range msg.ToolCalls {
|
||||
|
||||
tool := GetTool(tc.Function.Name)
|
||||
output, err := tool.Execute(ctx, []byte(tc.Function.Arguments))
|
||||
toolImpl := s.toolService.GetToolImpl(tc.Function.Name)
|
||||
slog.Info("Execute tool", "name", tc.Function.Name, "impl", toolImpl)
|
||||
toolRecord, err := s.toolService.GetToolByName(s.userID, s.teamID, toolImpl.Name())
|
||||
if err != nil {
|
||||
slog.Info("Execute tool", "error", err)
|
||||
toolResponse := &domain.Message{
|
||||
ChatID: s.chatID,
|
||||
TeamID: s.teamID,
|
||||
ToolCallID: tc.ID,
|
||||
Role: string(domain.RoleTool),
|
||||
Content: "ERROR: " + err.Error(),
|
||||
}
|
||||
s.repoChat.CreateChatMessage(s.userID, toolResponse)
|
||||
}
|
||||
output, err := toolImpl.Execute(ctx, []byte(tc.Function.Arguments), toolRecord.Configuration)
|
||||
var toolResponse *domain.Message
|
||||
if err != nil {
|
||||
toolResponse = &domain.Message{
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
You are an assistant that helps users by relying on the tools and skills made available to you. Reply in the user's language.
|
||||
|
||||
## Style: be direct
|
||||
|
||||
Answer precisely and concisely. Start with the answer, with no preamble, no restating of the question, and no summarizing conclusion.
|
||||
|
||||
Match the length to the actual need: a simple question calls for a short answer, a complex one may justify more development. Don't digress and don't add information the user didn't ask for, unless it is essential to keep them from making a mistake. If the answer fits in one sentence, one sentence is enough.
|
||||
|
||||
Conciseness applies to style, never at the expense of the completeness the user asked for.
|
||||
|
||||
## Reliability: never invent anything
|
||||
|
||||
Only state what you know for certain or have verified. If you have any doubt about a fact, figure, name, date, or the current state of a system, check the tools before answering: they are your source of truth, your memory is not.
|
||||
|
||||
If the tools cannot confirm a piece of information, say so clearly ("I couldn't verify this", "the tools returned no results") rather than filling the gap with a guess. Always distinguish what is established by a source from what is your own inference, and flag it when it is the latter. Saying "I don't know" is always better than giving an invented answer.
|
||||
|
||||
## Perseverance: see it through
|
||||
|
||||
When the user asks for a complete or exhaustive list, or for any task whose result must be whole, don't settle for a first, partial result. Check whether results are paginated or truncated, rerun the tools with other parameters, pages, or queries, and cross-check sources until you have covered the requested scope.
|
||||
|
||||
Never present a partial result as complete. If, despite your efforts, you cannot guarantee completeness (tool limitation, inaccessible source), state precisely what is missing or could not be verified. If a tool fails or returns an unusable result, try another approach before concluding.
|
||||
|
||||
## Tools and skills
|
||||
|
||||
Use the available tools and skills whenever they can help you answer more accurately, verify a fact, or complete the task. Don't answer from memory when a tool can provide the answer. If several tools are relevant, combine them. When a task matches an available skill, consult it before starting.
|
||||
|
||||
You may skip them for purely conversational exchanges or questions whose answer depends on no information that needs verifying.
|
||||
@@ -1,14 +0,0 @@
|
||||
package chat
|
||||
|
||||
import "trankilou.fr/lassistanoque/backend/internal/domain"
|
||||
|
||||
var allTools = make(map[string]domain.Tool)
|
||||
|
||||
func RegisterTool(tool domain.Tool) error {
|
||||
allTools[tool.Name()] = tool
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetTool(name string) domain.Tool {
|
||||
return allTools[name]
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo domain.ToolRepository
|
||||
repoUser domain.UserRepository
|
||||
allTools map[string]domain.ITool
|
||||
}
|
||||
|
||||
func NewService(
|
||||
repo domain.ToolRepository,
|
||||
repoUser domain.UserRepository,
|
||||
) *Service {
|
||||
return &Service{
|
||||
repo: repo,
|
||||
repoUser: repoUser,
|
||||
allTools: make(map[string]domain.ITool),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) ListTools(userID string, teamID string) ([]*domain.Tool, error) {
|
||||
|
||||
for _, tool := range s.allTools {
|
||||
_, err := s.repo.GetToolByName(userID, teamID, tool.Name())
|
||||
slog.Info("ListTools", "err1", err)
|
||||
if err != nil {
|
||||
_, err = s.repo.CreateTool(userID, &domain.Tool{
|
||||
TeamID: teamID,
|
||||
Name: tool.Name(),
|
||||
Enabled: true,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Info("ListTools", "err2", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s.repo.ListTools(userID, teamID)
|
||||
}
|
||||
|
||||
func (s *Service) GetTool(userID string, teamID string, id string) (*domain.Tool, error) {
|
||||
return s.repo.GetToolByID(userID, teamID, id)
|
||||
}
|
||||
|
||||
func (s *Service) GetToolByName(userID string, teamID string, name string) (*domain.Tool, error) {
|
||||
return s.repo.GetToolByName(userID, teamID, name)
|
||||
}
|
||||
|
||||
func (s *Service) CreateTool(userID string, Tool *domain.Tool) (*domain.Tool, error) {
|
||||
|
||||
if _, err := s.repoUser.FindUserTeam(userID, Tool.TeamID); err != nil {
|
||||
return nil, fmt.Errorf("error finding user in team: %s", err)
|
||||
}
|
||||
|
||||
return s.repo.CreateTool(userID, Tool)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateTool(userID string, Tool *domain.Tool) (*domain.Tool, error) {
|
||||
if _, err := s.repoUser.FindUserTeam(userID, Tool.TeamID); err != nil {
|
||||
return nil, fmt.Errorf("error finding user in team: %s", err)
|
||||
}
|
||||
|
||||
if _, err := s.repo.GetToolByID(userID, Tool.TeamID, Tool.ID); err != nil {
|
||||
return nil, fmt.Errorf("error finding Tool: %s", err)
|
||||
}
|
||||
|
||||
return s.repo.UpdateTool(userID, Tool)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteTool(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.GetToolByID(userID, teamID, id); err != nil {
|
||||
return fmt.Errorf("error finding Tool: %s", err)
|
||||
}
|
||||
|
||||
return s.repo.DeleteTool(userID, teamID, id)
|
||||
|
||||
}
|
||||
|
||||
func (s *Service) RegisterToolImpl(tool domain.ITool) error {
|
||||
s.allTools[tool.Name()] = tool
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) GetAllToolImpl() map[string]domain.ITool {
|
||||
return s.allTools
|
||||
}
|
||||
|
||||
func (s *Service) GetToolImpl(name string) domain.ITool {
|
||||
return s.allTools[name]
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
import { api } from '../client';
|
||||
import type { Tool, ToolParam } from "$lib/types/api"
|
||||
|
||||
export const toolApi = {
|
||||
getTools: (space: string) => api.get<Tool[]>(`/tool/${space}`),
|
||||
getTool: (space: string, id: string) => api.get<Tool>(`/tool/${space}/${id}`),
|
||||
getToolParams: (space: string, id: string) => api.get<ToolParam[]>(`/tool/${space}/${id}/params`),
|
||||
createTool: (space: string,data: Partial<Tool>) => api.post<Tool>(`/tool/${space}`, data),
|
||||
updateTool: (space: string,data: Partial<Tool>) => api.put<Tool>(`/tool/${space}`, data),
|
||||
};
|
||||
@@ -104,6 +104,24 @@ export interface Message {
|
||||
_version: string
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
id: string
|
||||
teamId: string
|
||||
name: string
|
||||
configuration: string
|
||||
enabled: boolean
|
||||
_date_created: string
|
||||
_date_updated: string
|
||||
_version: string
|
||||
}
|
||||
|
||||
export interface ToolParam {
|
||||
name: string
|
||||
type: string
|
||||
description: string
|
||||
mandatory: boolean
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
ID: string
|
||||
Type: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {autofocus} from "$lib/ui/autofocus"
|
||||
import { onMount } from "svelte"
|
||||
import {onMount, tick } from "svelte"
|
||||
import {page} from "$app/state"
|
||||
import {chatApi} from "$lib/api/endpoints/chat"
|
||||
import type { PageProps } from './$types';
|
||||
@@ -20,9 +20,12 @@
|
||||
let chatWithMessage : ChatWithMessages = await chatApi.getChat(data.spaceId, data.chatId)
|
||||
messages = chatWithMessage.messages
|
||||
}
|
||||
await tick()
|
||||
historyDiv.scrollTop = historyDiv.scrollHeight;
|
||||
})
|
||||
|
||||
let historyDiv: HTMLDivElement;
|
||||
let inputPrompt: HTMLDivElement;
|
||||
|
||||
const sendRequest = async(theprompt:string) => {
|
||||
prompt = ""
|
||||
@@ -34,6 +37,12 @@
|
||||
if (waiting && chunk.role==="assistant") {
|
||||
waiting = false
|
||||
}
|
||||
if (chunk.done) {
|
||||
document
|
||||
.querySelectorAll<HTMLDivElement>('div.reasoning:not(.hidden)')
|
||||
.forEach((el) => el.classList.add('hidden'));
|
||||
return
|
||||
}
|
||||
|
||||
if (chunk.role!=lastrole || chunk.toolName!=lastToolName) {
|
||||
const currentDate = new Date()
|
||||
@@ -80,7 +89,9 @@
|
||||
}
|
||||
|
||||
const handleOnSubmit = async ()=>{
|
||||
historyDiv.scrollTop = historyDiv.scrollHeight;
|
||||
await sendRequest(prompt)
|
||||
inputPrompt.focus()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -94,7 +105,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#if message.toolcalls != null && message.toolcalls.length>0}
|
||||
<div class="text-green-600">⚒️ Appel de l'outil {message.toolcalls[0].Function.Name}</div>
|
||||
<div class="tool text-sm mt-2 py-1 px-2 rounded">⚒️ Appel de l'outil {message.toolcalls[0].Function.Name}</div>
|
||||
{:else}
|
||||
<div class="message-content">
|
||||
<SvelteMarkdown source={message.content} streaming={true}/>
|
||||
@@ -114,6 +125,7 @@
|
||||
placeholder="..."
|
||||
bind:value={prompt}
|
||||
use:autofocus
|
||||
bind:this={inputPrompt}
|
||||
/>
|
||||
<button class="chatbutton">Go</button>
|
||||
</form>
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
href="settings/tools"
|
||||
>
|
||||
Outils
|
||||
<span class="ml-2 text-xs bg-red-900 text-red-300 rounded-lg p-1">12</span>
|
||||
<!-- <span class="ml-2 text-xs bg-red-900 text-red-300 rounded-lg p-1">12</span>-->
|
||||
</a>
|
||||
|
||||
<!--
|
||||
<a class="bt p-5 text-center"
|
||||
title="Skills"
|
||||
href="settings/skills">
|
||||
@@ -58,4 +59,6 @@
|
||||
Utilisateurs
|
||||
<span class="ml-2 text-xs bg-red-900 text-red-300 rounded-lg p-1">1</span>
|
||||
</a>
|
||||
|
||||
-->
|
||||
</div>
|
||||
@@ -108,7 +108,6 @@
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
event.preventDefault()
|
||||
if (event.key==="Escape") {
|
||||
handleCancelEditAgent()
|
||||
}
|
||||
@@ -149,7 +148,7 @@
|
||||
/>
|
||||
{/key}
|
||||
{:else}
|
||||
<div class="rounded border p-2 my-2 border-stone-500 flex">
|
||||
<div class="rounded border p-2 my-2 border-stone-500 flex formgroup">
|
||||
<div class="flex-1 p-2">
|
||||
<span>{ agent.name }</span>
|
||||
</div>
|
||||
|
||||
@@ -102,7 +102,6 @@
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
event.preventDefault()
|
||||
if (event.key==="Escape") {
|
||||
cancelEditProviderHandler()
|
||||
}
|
||||
@@ -146,7 +145,7 @@
|
||||
/>
|
||||
{/key}
|
||||
{:else}
|
||||
<div class="rounded border p-2 my-2 border-stone-500 flex">
|
||||
<div class="rounded border p-2 my-2 border-stone-500 flex formgroup">
|
||||
<div class="flex-1 p-2">
|
||||
<span>{ p.name }</span>
|
||||
<span class="text-stone-500">({ p.type })</span>
|
||||
|
||||
@@ -1 +1,96 @@
|
||||
<div>Modèles</div>
|
||||
<script lang="ts">
|
||||
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
import type { Tool } from '$lib/types/api'
|
||||
import type { PageProps } from './$types';
|
||||
import { toolApi } from "$lib/api/endpoints/tool"
|
||||
|
||||
//import { validateUrl } from '$lib/utilities/validators'
|
||||
import ToolForm from './ToolForm.svelte'
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
|
||||
let tools = $state(untrack(()=>data.tools))
|
||||
let adding = $state(false)
|
||||
let editing = $state(null as string|null)
|
||||
let errors = new SvelteMap<string,string>()
|
||||
|
||||
const cancelEditHandler = () => {
|
||||
editing = null
|
||||
}
|
||||
|
||||
const submitEdit = (tool: Tool) => {
|
||||
|
||||
if (errors.size === 0) {
|
||||
toolApi.updateTool(data.space,tool)
|
||||
.then((t)=>{
|
||||
tools = tools.map((it)=>{return t.id===it.id ? t : it})
|
||||
editing = null
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = async (id: string) => {
|
||||
errors.clear()
|
||||
if (adding) return
|
||||
toolApi.getTool(data.space,id)
|
||||
.then((data)=>{
|
||||
tools = tools.map((it)=>it.id===id ? data : it)
|
||||
editing = id
|
||||
})
|
||||
}
|
||||
|
||||
// const handleDelete = (id: string) => {
|
||||
// toolApi.deleteTool(data.space,id)
|
||||
// .then(() => {
|
||||
// tools = tools.filter((it)=>it.id!==id)
|
||||
// })
|
||||
// .catch((err) => {console.log(err)})
|
||||
// }
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key==="Escape") {
|
||||
cancelEditHandler()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<div class="container">
|
||||
|
||||
{#each tools as t (t.id) }
|
||||
{#if editing===t.id}
|
||||
{#key t.id}
|
||||
<ToolForm
|
||||
item={t}
|
||||
errors={errors}
|
||||
onSave={submitEdit}
|
||||
onCancel={cancelEditHandler}
|
||||
/>
|
||||
{/key}
|
||||
{:else}
|
||||
<div class="rounded border p-2 my-2 border-stone-500 flex formgroup">
|
||||
<div class="flex-1 p-2">
|
||||
<span>{ t.name }</span>
|
||||
<span class="text-stone-500">({ t.enabled })</span>
|
||||
</div>
|
||||
<button
|
||||
class="bt small mx-1"
|
||||
onclick={()=>handleEdit(t.id)}
|
||||
aria-label="Modifier"
|
||||
>
|
||||
<i class="icon-edit"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
</div>
|
||||
@@ -1,6 +1,11 @@
|
||||
export function load() {
|
||||
import { toolApi } from "$lib/api/endpoints/tool"
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
return {
|
||||
title: 'Paramétrage : Outils',
|
||||
back: "/settings",
|
||||
back: "../settings",
|
||||
space: params.space,
|
||||
tools: await toolApi.getTools(params.space),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
|
||||
import FormError from '$lib/components/FormError.svelte'
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
import { toolApi } from "$lib/api/endpoints/tool"
|
||||
import type { Tool, ToolParam } from "$lib/types/api"
|
||||
import { onMount, untrack } from 'svelte';
|
||||
|
||||
type Props = {
|
||||
item: Tool
|
||||
errors: Map<string,string>
|
||||
onSave: (item: Tool) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
let {
|
||||
item,
|
||||
errors,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: Props = $props();
|
||||
|
||||
let tool = $state(untrack(()=>item))
|
||||
let toolParams = $state([] as ToolParam[])
|
||||
let paramValues = new SvelteMap<string, string>();
|
||||
|
||||
onMount(async () => {
|
||||
toolParams = await toolApi.getToolParams(item.teamId, item.id)
|
||||
paramValues = new SvelteMap<string, string>(Object.entries(JSON.parse(item.configuration)));
|
||||
})
|
||||
|
||||
const autofocus = (node: HTMLElement) => {
|
||||
node.focus();
|
||||
}
|
||||
|
||||
const handleSave = (e: Event) => {
|
||||
e.preventDefault()
|
||||
item.configuration = JSON.stringify(Object.fromEntries(paramValues));
|
||||
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={tool.name}
|
||||
class="textinput"
|
||||
placeholder="nom affiché"
|
||||
disabled
|
||||
use:autofocus
|
||||
/>
|
||||
<FormError key="name" errors={errors} />
|
||||
</div>
|
||||
|
||||
<div class="formelement">
|
||||
<label for="enabled">Actif</label>
|
||||
<input
|
||||
id="enabled"
|
||||
type="checkbox"
|
||||
bind:checked={tool.enabled}
|
||||
class="checkboxinput"
|
||||
/>
|
||||
<FormError key="baseUrl" errors={errors} />
|
||||
</div>
|
||||
|
||||
|
||||
{#each toolParams as param (param.name)}
|
||||
|
||||
<div class="formelement">
|
||||
<label for={param.name}>{param.name}</label>
|
||||
<input
|
||||
id={param.name}
|
||||
bind:value={() => paramValues.get(param.name) ?? '', (v) => paramValues.set(param.name, v)}
|
||||
class="textinput"
|
||||
placeholder={param.description}
|
||||
/>
|
||||
<FormError key="baseUrl" errors={errors} />
|
||||
</div>
|
||||
|
||||
{/each}
|
||||
|
||||
|
||||
<div class="text-right">
|
||||
<button type="button" class="bt" onclick={handleCancel}>Annuler</button>
|
||||
<button type="submit" class="bt mx-4">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,6 +137,10 @@ h2 {
|
||||
@apply h-8 w-full rounded border-stone-300 bg-stone-50 px-2 py-1 text-sm text-stone-900 dark:border-stone-700 dark:bg-stone-950 dark:text-stone-100;
|
||||
}
|
||||
|
||||
.checkboxinput {
|
||||
@apply rounded border border-stone-500 bg-stone-100 text-sm text-stone-900 dark:bg-stone-900 dark:text-stone-100;
|
||||
}
|
||||
|
||||
.areainput {
|
||||
@apply w-full rounded border-stone-300 bg-stone-50 px-2 py-1 text-sm text-stone-900 dark:bg-stone-950 dark:text-stone-100;
|
||||
}
|
||||
@@ -275,25 +279,60 @@ h2 {
|
||||
/* chat */
|
||||
|
||||
.message {
|
||||
@apply rounded p-2;
|
||||
|
||||
&.user {
|
||||
@apply bg-sky-700 dark:text-white;
|
||||
@apply bg-sky-500/20 px-2 py-1 text-sky-700 dark:text-sky-300;
|
||||
}
|
||||
.tool {
|
||||
@apply bg-green-500/20 px-2 py-1 text-green-700 dark:text-green-300;
|
||||
}
|
||||
|
||||
hr {
|
||||
@apply my-4;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
@apply my-2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-2xl font-bold;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-xl font-bold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-lg font-bold;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-base;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
li {
|
||||
@apply ml-5 list-disc pl-4 marker:text-xl;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply my-2 rounded px-2 py-1 text-xs dark:bg-stone-700;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply my-2;
|
||||
}
|
||||
}
|
||||
|
||||
.message-content {
|
||||
@apply text-sm;
|
||||
@apply whitespace-pre-line;
|
||||
|
||||
&.reasoning {
|
||||
@apply text-white/50;
|
||||
}
|
||||
}
|
||||
|
||||
.message-content p {
|
||||
@apply my-2;
|
||||
}
|
||||
|
||||
.message-content pre {
|
||||
@apply my-2 whitespace-pre dark:bg-stone-700;
|
||||
}
|
||||
Reference in new issue
Block a user