Compare commits

...
2 Commits
Author SHA1 Message Date
fabien 345129c7bf Ajout de SEARXNG pour les recherches web 2026-10-05 19:05:24 +02:00
fabien 87eeb41364 paufinement de l'UI du chat 2026-10-04 21:55:31 +02:00
33 changed files with 1010 additions and 177 deletions

No files matched your search

-105
View File
@@ -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()
}
+145
View File
@@ -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
View File
@@ -11,14 +11,8 @@ import (
"time" "time"
"github.com/spf13/cobra" "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/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() { func init() {
@@ -44,35 +38,26 @@ func runServe() {
defer Close() 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 // http server
httpRouter := http.NewRouter(http.Dependencies{ httpRouter := http.NewRouter(http.Dependencies{
StorageService: storageService, StorageService: GetStorageService(),
AuthService: authService, AuthService: GetAuthService(),
UserService: userService, UserService: GetUserService(),
ProviderService: providerService, ProviderService: GetProviderService(),
AgentService: agentService, AgentService: GetAgentService(),
ChatService: chatService, ChatService: GetChatService(),
ToolService: GetToolService(),
TokenManager: GetTokenManager(), TokenManager: GetTokenManager(),
}) })
brainRouter := brain.NewRouter(brain.Dependencies{ gatewayRouter := gateway.NewRouter(gateway.Dependencies{})
ChatRepository: GetChatRepository(),
})
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() defer stop()
// start Brain router // start Brain router
go func() { 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) slog.Error("failed to start llm server", "error", err)
} }
}() }()
@@ -97,7 +82,7 @@ func runServe() {
// stop LLM service // stop LLM service
ctx, cancel2 := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel2 := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel2() 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.Error("failed to stop llm server", "error", err)
} }
slog.Info("LLM server stopped gracefully") slog.Info("LLM server stopped gracefully")
@@ -15,6 +15,7 @@ type Database interface {
ProviderRepository() domain.ProviderRepository ProviderRepository() domain.ProviderRepository
AgentRepository() domain.AgentRepository AgentRepository() domain.AgentRepository
ChatRepository() domain.ChatRepository ChatRepository() domain.ChatRepository
ToolRepository() domain.ToolRepository
Migrate() error Migrate() error
Close() Close()
} }
@@ -79,3 +79,7 @@ func (db *TursoDB) AgentRepository() domain.AgentRepository {
func (db *TursoDB) ChatRepository() domain.ChatRepository { func (db *TursoDB) ChatRepository() domain.ChatRepository {
return NewTursoChatRepository(db.DB) 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, id text not null primary key,
team_id text not null, team_id text not null,
name text not null, name text not null,
type text not null,
enabled numeric not null default 1, 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_created numeric not null default current_timestamp,
_date_updated numeric, _date_updated numeric,
_version text not null _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,
)
}
+1
View File
@@ -2,6 +2,7 @@ package llm
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog" "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, &params); 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" 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 { func (t *WeatherTool) Definition(ctx context.Context) *domain.ToolDefinition {
return &domain.ToolDefinition{ return &domain.ToolDefinition{
Type: "function", 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 { var params struct {
Location string `json:"location"` Location string `json:"location"`
+1
View File
@@ -29,6 +29,7 @@ type Config struct {
StorageS3AccessKeySecret string // S3 StorageS3AccessKeySecret string // S3
StorageS3UseSSL string // S3 StorageS3UseSSL string // S3
ServerID string ServerID string
SearXngUrl string
} }
var config *Config var config *Config
+10 -2
View File
@@ -66,11 +66,19 @@ type ChatParams struct {
OnChunk StreamCallback 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 Name() string
Description() string Description() string
Params() []ToolParam
Definition(ctx context.Context) *ToolDefinition 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 { type ToolDefinition struct {
+23
View File
@@ -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 ( import (
"context" "context"
@@ -1,4 +1,4 @@
package brain package gateway
type Session struct { type Session struct {
serverID string serverID string
+123
View File
@@ -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)
// }
+3
View File
@@ -16,6 +16,7 @@ import (
"trankilou.fr/lassistanoque/backend/internal/service/chat" "trankilou.fr/lassistanoque/backend/internal/service/chat"
"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"
"trankilou.fr/lassistanoque/backend/internal/service/tool"
"trankilou.fr/lassistanoque/backend/internal/service/user" "trankilou.fr/lassistanoque/backend/internal/service/user"
) )
@@ -42,6 +43,7 @@ type Dependencies struct {
ProviderService *provider.Service ProviderService *provider.Service
AgentService *agent.Service AgentService *agent.Service
ChatService *chat.Service ChatService *chat.Service
ToolService *tool.Service
TokenManager auth.TokenManager TokenManager auth.TokenManager
} }
@@ -82,6 +84,7 @@ func NewRouter(deps Dependencies) *Router {
_ = 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.UserService, 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.NewTeamGroup("/team", api, deps.UserService, deps.TokenManager.TokenMiddleware)
_ = handlers.NewChatGroup("/chat", api, deps.ChatService, deps.TokenManager.TokenMiddleware) _ = handlers.NewChatGroup("/chat", api, deps.ChatService, deps.TokenManager.TokenMiddleware)
+5
View File
@@ -8,6 +8,7 @@ import (
"time" "time"
"trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/tool"
) )
type Service struct { type Service struct {
@@ -17,6 +18,7 @@ type Service struct {
repoChat domain.ChatRepository repoChat domain.ChatRepository
llmEngine domain.LLMEngine llmEngine domain.LLMEngine
runningSessions map[string]*ChatSession runningSessions map[string]*ChatSession
toolService *tool.Service
mu sync.Mutex mu sync.Mutex
} }
@@ -26,6 +28,7 @@ func NewService(
repoProvider domain.ProviderRepository, repoProvider domain.ProviderRepository,
repoChat domain.ChatRepository, repoChat domain.ChatRepository,
llmEngine domain.LLMEngine, llmEngine domain.LLMEngine,
toolService *tool.Service,
) *Service { ) *Service {
return &Service{ return &Service{
repoUser: repoUser, repoUser: repoUser,
@@ -33,6 +36,7 @@ func NewService(
repoProvider: repoProvider, repoProvider: repoProvider,
repoChat: repoChat, repoChat: repoChat,
llmEngine: llmEngine, llmEngine: llmEngine,
toolService: toolService,
runningSessions: make(map[string]*ChatSession), runningSessions: make(map[string]*ChatSession),
} }
} }
@@ -194,6 +198,7 @@ func (s *Service) runQuery(
modelID: modelID, modelID: modelID,
messages: messages, messages: messages,
llmEngine: s.llmEngine, llmEngine: s.llmEngine,
toolService: s.toolService,
subscribers: make([]domain.StreamCallback, 0), subscribers: make([]domain.StreamCallback, 0),
repoChat: s.repoChat, repoChat: s.repoChat,
} }
+24 -4
View File
@@ -2,11 +2,17 @@ package chat
import ( import (
"context" "context"
_ "embed"
"log/slog"
"sync" "sync"
"trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/tool"
) )
//go:embed system_prompt.md
var systemSystemPrompt string
type ChatSession struct { type ChatSession struct {
userID string userID string
teamID string teamID string
@@ -16,6 +22,7 @@ type ChatSession struct {
modelID string modelID string
messages []*domain.Message messages []*domain.Message
llmEngine domain.LLMEngine llmEngine domain.LLMEngine
toolService *tool.Service
repoChat domain.ChatRepository repoChat domain.ChatRepository
subscribers []domain.StreamCallback subscribers []domain.StreamCallback
mu sync.Mutex mu sync.Mutex
@@ -26,7 +33,7 @@ func (s *ChatSession) run(ctx context.Context) error {
params := &domain.LLMParams{} params := &domain.LLMParams{}
tools := make([]*domain.ToolDefinition, 0) tools := make([]*domain.ToolDefinition, 0)
for _, t := range allTools { for _, t := range s.toolService.GetAllToolImpl() {
tools = append(tools, t.Definition(ctx)) tools = append(tools, t.Definition(ctx))
} }
params.Tools = tools params.Tools = tools
@@ -43,7 +50,7 @@ func (s *ChatSession) run(ctx context.Context) error {
systemPrompt := &domain.Message{ systemPrompt := &domain.Message{
Role: string(domain.RoleSystem), Role: string(domain.RoleSystem),
Content: s.agent.SystemPrompt, Content: systemSystemPrompt + "\n\n## Customization\n\n" + s.agent.SystemPrompt,
} }
continue_loop := true continue_loop := true
@@ -72,8 +79,21 @@ func (s *ChatSession) run(ctx context.Context) error {
continue_loop = true continue_loop = true
for _, tc := range msg.ToolCalls { for _, tc := range msg.ToolCalls {
tool := GetTool(tc.Function.Name) toolImpl := s.toolService.GetToolImpl(tc.Function.Name)
output, err := tool.Execute(ctx, []byte(tc.Function.Arguments)) 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 var toolResponse *domain.Message
if err != nil { if err != nil {
toolResponse = &domain.Message{ 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.
-14
View File
@@ -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]
}
+100
View File
@@ -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.
+10
View File
@@ -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),
};
+18
View File
@@ -104,6 +104,24 @@ export interface Message {
_version: string _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 { export interface ToolCall {
ID: string ID: string
Type: string Type: string
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {autofocus} from "$lib/ui/autofocus" import {autofocus} from "$lib/ui/autofocus"
import { onMount } from "svelte" import {onMount, tick } from "svelte"
import {page} from "$app/state" import {page} from "$app/state"
import {chatApi} from "$lib/api/endpoints/chat" import {chatApi} from "$lib/api/endpoints/chat"
import type { PageProps } from './$types'; import type { PageProps } from './$types';
@@ -20,9 +20,12 @@
let chatWithMessage : ChatWithMessages = await chatApi.getChat(data.spaceId, data.chatId) let chatWithMessage : ChatWithMessages = await chatApi.getChat(data.spaceId, data.chatId)
messages = chatWithMessage.messages messages = chatWithMessage.messages
} }
await tick()
historyDiv.scrollTop = historyDiv.scrollHeight;
}) })
let historyDiv: HTMLDivElement; let historyDiv: HTMLDivElement;
let inputPrompt: HTMLDivElement;
const sendRequest = async(theprompt:string) => { const sendRequest = async(theprompt:string) => {
prompt = "" prompt = ""
@@ -34,6 +37,12 @@
if (waiting && chunk.role==="assistant") { if (waiting && chunk.role==="assistant") {
waiting = false 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) { if (chunk.role!=lastrole || chunk.toolName!=lastToolName) {
const currentDate = new Date() const currentDate = new Date()
@@ -80,7 +89,9 @@
} }
const handleOnSubmit = async ()=>{ const handleOnSubmit = async ()=>{
historyDiv.scrollTop = historyDiv.scrollHeight;
await sendRequest(prompt) await sendRequest(prompt)
inputPrompt.focus()
} }
</script> </script>
@@ -94,7 +105,7 @@
</div> </div>
{/if} {/if}
{#if message.toolcalls != null && message.toolcalls.length>0} {#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} {:else}
<div class="message-content"> <div class="message-content">
<SvelteMarkdown source={message.content} streaming={true}/> <SvelteMarkdown source={message.content} streaming={true}/>
@@ -114,6 +125,7 @@
placeholder="..." placeholder="..."
bind:value={prompt} bind:value={prompt}
use:autofocus use:autofocus
bind:this={inputPrompt}
/> />
<button class="chatbutton">Go</button> <button class="chatbutton">Go</button>
</form> </form>
@@ -17,9 +17,10 @@
href="settings/tools" href="settings/tools"
> >
Outils 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>
<!--
<a class="bt p-5 text-center" <a class="bt p-5 text-center"
title="Skills" title="Skills"
href="settings/skills"> href="settings/skills">
@@ -58,4 +59,6 @@
Utilisateurs Utilisateurs
<span class="ml-2 text-xs bg-red-900 text-red-300 rounded-lg p-1">1</span> <span class="ml-2 text-xs bg-red-900 text-red-300 rounded-lg p-1">1</span>
</a> </a>
-->
</div> </div>
@@ -108,7 +108,6 @@
} }
const handleKeydown = (event: KeyboardEvent) => { const handleKeydown = (event: KeyboardEvent) => {
event.preventDefault()
if (event.key==="Escape") { if (event.key==="Escape") {
handleCancelEditAgent() handleCancelEditAgent()
} }
@@ -149,7 +148,7 @@
/> />
{/key} {/key}
{:else} {: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"> <div class="flex-1 p-2">
<span>{ agent.name }</span> <span>{ agent.name }</span>
</div> </div>
@@ -102,7 +102,6 @@
} }
const handleKeydown = (event: KeyboardEvent) => { const handleKeydown = (event: KeyboardEvent) => {
event.preventDefault()
if (event.key==="Escape") { if (event.key==="Escape") {
cancelEditProviderHandler() cancelEditProviderHandler()
} }
@@ -146,7 +145,7 @@
/> />
{/key} {/key}
{:else} {: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"> <div class="flex-1 p-2">
<span>{ p.name }</span> <span>{ p.name }</span>
<span class="text-stone-500">({ p.type })</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 { return {
title: 'Paramétrage : Outils', 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>
+50 -11
View File
@@ -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; @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 { .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; @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 */ /* chat */
.message { .message {
@apply rounded p-2;
&.user { &.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 { .message-content {
@apply text-sm; @apply text-sm;
@apply whitespace-pre-line;
&.reasoning { &.reasoning {
@apply text-white/50; @apply text-white/50;
} }
} }
.message-content p {
@apply my-2;
}
.message-content pre {
@apply my-2 whitespace-pre dark:bg-stone-700;
}