Mise à jour du titre (imparfait)
This commit is contained in:
1 parent
345129c7bf
commit
509a3b14c8
14 files changed
+445
-12
No files matched your search
@@ -8,6 +8,7 @@ import (
|
|||||||
"trankilou.fr/lassistanoque/backend/internal/adapter/file"
|
"trankilou.fr/lassistanoque/backend/internal/adapter/file"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/adapter/llm"
|
"trankilou.fr/lassistanoque/backend/internal/adapter/llm"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/adapter/security"
|
"trankilou.fr/lassistanoque/backend/internal/adapter/security"
|
||||||
|
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/browser"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/searxng"
|
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/searxng"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/weather"
|
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/weather"
|
||||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||||
@@ -140,6 +141,7 @@ func GetToolService() *tool.Service {
|
|||||||
toolService = tool.NewService(db.ToolRepository(), db.UserRepository())
|
toolService = tool.NewService(db.ToolRepository(), db.UserRepository())
|
||||||
toolService.RegisterToolImpl(&weather.WeatherTool{})
|
toolService.RegisterToolImpl(&weather.WeatherTool{})
|
||||||
toolService.RegisterToolImpl(&searxng.SearxngTool{})
|
toolService.RegisterToolImpl(&searxng.SearxngTool{})
|
||||||
|
toolService.RegisterToolImpl(&browser.BrowserTool{})
|
||||||
}
|
}
|
||||||
return toolService
|
return toolService
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,15 @@ func (r *TursoChatRepository) ListChats(userID string, teamID string, page int)
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *TursoChatRepository) ListChatsWithUnfreshTitles(teamID string) ([]*domain.Chat, error) {
|
||||||
|
return r.chatTable.Select(
|
||||||
|
orm.WithWhere(
|
||||||
|
"team_id=$1 and fresh_title=0",
|
||||||
|
teamID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *TursoChatRepository) GetChat(userID string, teamID string, id string) (*domain.Chat, error) {
|
func (r *TursoChatRepository) GetChat(userID string, teamID string, id string) (*domain.Chat, error) {
|
||||||
return r.chatTable.SelectOne(
|
return r.chatTable.SelectOne(
|
||||||
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
|
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type TursoDB struct {
|
|||||||
func NewTursoDB(cfg *config.Config) (*TursoDB, error) {
|
func NewTursoDB(cfg *config.Config) (*TursoDB, error) {
|
||||||
db, err := sql.Open("turso", cfg.DatabaseURL)
|
db, err := sql.Open("turso", cfg.DatabaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error connecting to db %: %s", cfg.DatabaseURL, err)
|
return nil, fmt.Errorf("error connecting to db %s: %s", cfg.DatabaseURL, err)
|
||||||
}
|
}
|
||||||
return &TursoDB{
|
return &TursoDB{
|
||||||
DB: db,
|
DB: db,
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ create table chats (
|
|||||||
user_id text,
|
user_id text,
|
||||||
task_id text,
|
task_id text,
|
||||||
title text,
|
title text,
|
||||||
|
fresh_title numeric default 0,
|
||||||
_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
|
||||||
|
|||||||
@@ -203,6 +203,40 @@ func (e *AnyLLMEngine) Stream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *AnyLLMEngine) Generate(
|
||||||
|
ctx context.Context,
|
||||||
|
provider *domain.Provider,
|
||||||
|
modelID string,
|
||||||
|
messages []*domain.Message,
|
||||||
|
) (string, error) {
|
||||||
|
|
||||||
|
p, err := providerFactory(provider)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
anyllmMessages := make([]anyllm.Message, 0)
|
||||||
|
for _, m := range messages {
|
||||||
|
anyllmMessages = append(anyllmMessages, anyllm.Message{
|
||||||
|
Role: m.Role,
|
||||||
|
Content: m.Content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := p.Completion(ctx, anyllm.CompletionParams{
|
||||||
|
Model: modelID,
|
||||||
|
Messages: anyllmMessages,
|
||||||
|
Stream: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Choices[0].Message.Content.(string), nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func toolConvert(domainTool *domain.ToolDefinition) providers.Tool {
|
func toolConvert(domainTool *domain.ToolDefinition) providers.Tool {
|
||||||
return providers.Tool{
|
return providers.Tool{
|
||||||
Type: domainTool.Type,
|
Type: domainTool.Type,
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
package browser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-rod/rod"
|
||||||
|
"github.com/go-rod/rod/lib/launcher"
|
||||||
|
"github.com/go-rod/rod/lib/proto"
|
||||||
|
"github.com/go-rod/stealth"
|
||||||
|
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BrowserConfig struct {
|
||||||
|
// Headless est un pointeur pour distinguer "non renseigné" (défaut : true) de "explicitement false".
|
||||||
|
Headless *bool `yaml:"headless"`
|
||||||
|
TimeoutSeconds int `yaml:"timeoutSeconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BrowserTool maintains a persistent browser session across LLM tool calls.
|
||||||
|
type BrowserTool struct{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
browserInstance *rod.Browser
|
||||||
|
currentPage *rod.Page
|
||||||
|
browserMu sync.Mutex
|
||||||
|
launchedHeadless bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *BrowserTool) Name() string {
|
||||||
|
return "browser"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *BrowserTool) Description() string {
|
||||||
|
return "Control a web browser to navigate pages and interact with content. " +
|
||||||
|
"Maintains session state across calls (cookies, current page). " +
|
||||||
|
"Actions: 'navigate' (go to URL), 'click' (click element by CSS selector), " +
|
||||||
|
"'fill' (type into an input field), 'content' (get current page text), " +
|
||||||
|
"'links' (list all links on current page)."
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *BrowserTool) Params() []domain.ToolParam {
|
||||||
|
return []domain.ToolParam{
|
||||||
|
{JsonName: "headless", Type: "bool", Mandatory: true, Description: "Headless"},
|
||||||
|
{JsonName: "timeoutSeconds", Type: "int", Mandatory: true, Description: "Timeout en secondes"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *BrowserTool) 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{
|
||||||
|
"action": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"enum": []string{"navigate", "click", "fill", "content", "links"},
|
||||||
|
"description": "The browser action to perform.",
|
||||||
|
},
|
||||||
|
"url": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "URL to navigate to. Required for action='navigate'.",
|
||||||
|
},
|
||||||
|
"selector": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "CSS selector of the element to interact with. Required for 'click' and 'fill'.",
|
||||||
|
},
|
||||||
|
"value": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Text value to type into the field. Required for action='fill'.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"action"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *BrowserTool) Execute(_ context.Context, input []byte, configuration string) ([]byte, error) {
|
||||||
|
|
||||||
|
slog.Debug("[browser]", "input", string(input))
|
||||||
|
|
||||||
|
var params struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Selector string `json:"selector"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(input, ¶ms); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid parameters: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var toolConfig BrowserConfig
|
||||||
|
err := json.Unmarshal([]byte(configuration), &toolConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
timeout := time.Duration(toolConfig.TimeoutSeconds) * time.Second
|
||||||
|
|
||||||
|
// Headless est nil quand la configuration ne le précise pas : défaut à true.
|
||||||
|
headless := true
|
||||||
|
if toolConfig.Headless != nil {
|
||||||
|
headless = *toolConfig.Headless
|
||||||
|
}
|
||||||
|
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 60 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
browserMu.Lock()
|
||||||
|
defer browserMu.Unlock()
|
||||||
|
|
||||||
|
if err := ensureBrowser(headless); err != nil {
|
||||||
|
return nil, fmt.Errorf("browser init failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch params.Action {
|
||||||
|
case "navigate":
|
||||||
|
if params.URL == "" {
|
||||||
|
return nil, fmt.Errorf("'url' is required for action 'navigate'")
|
||||||
|
}
|
||||||
|
return t.navigate(params.URL, timeout)
|
||||||
|
|
||||||
|
case "click":
|
||||||
|
if params.Selector == "" {
|
||||||
|
return nil, fmt.Errorf("'selector' is required for action 'click'")
|
||||||
|
}
|
||||||
|
return t.click(params.Selector, timeout)
|
||||||
|
|
||||||
|
case "fill":
|
||||||
|
if params.Selector == "" || params.Value == "" {
|
||||||
|
return nil, fmt.Errorf("'selector' and 'value' are required for action 'fill'")
|
||||||
|
}
|
||||||
|
return t.fill(params.Selector, params.Value, timeout)
|
||||||
|
|
||||||
|
case "content":
|
||||||
|
return t.pageContent(timeout)
|
||||||
|
|
||||||
|
case "links":
|
||||||
|
return t.pageLinks(timeout)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown action %q", params.Action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func browserProfileDir() string {
|
||||||
|
if dir, err := os.UserCacheDir(); err == nil {
|
||||||
|
return filepath.Join(dir, "lassistanoque", "browser-profile")
|
||||||
|
}
|
||||||
|
return filepath.Join(os.TempDir(), "lassistanoque-browser-profile")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureBrowser lance le navigateur au premier appel et le relance si le mode
|
||||||
|
// headless a changé dans la configuration.
|
||||||
|
func ensureBrowser(headless bool) error {
|
||||||
|
if browserInstance != nil {
|
||||||
|
if headless == launchedHeadless {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = browserInstance.Close()
|
||||||
|
browserInstance = nil
|
||||||
|
currentPage = nil
|
||||||
|
}
|
||||||
|
if err := initBrowser(headless); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
launchedHeadless = headless
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initBrowser(headless bool) error {
|
||||||
|
path, _ := launcher.LookPath()
|
||||||
|
controlURL, err := launcher.New().
|
||||||
|
Bin(path).
|
||||||
|
Headless(headless).
|
||||||
|
UserDataDir(browserProfileDir()).
|
||||||
|
Delete("enable-automation").
|
||||||
|
Launch()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to launch browser: %w", err)
|
||||||
|
}
|
||||||
|
b := rod.New().ControlURL(controlURL)
|
||||||
|
if err := b.Connect(); err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to browser: %w", err)
|
||||||
|
}
|
||||||
|
browserInstance = b
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensurePage(timeout time.Duration) error {
|
||||||
|
if currentPage == nil {
|
||||||
|
page, err := stealth.Page(browserInstance.Timeout(timeout))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open page: %w", err)
|
||||||
|
}
|
||||||
|
currentPage = page
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *BrowserTool) navigate(rawURL string, timeout time.Duration) ([]byte, error) {
|
||||||
|
if err := ensurePage(timeout); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := currentPage.Timeout(timeout).Navigate(rawURL); err != nil {
|
||||||
|
return nil, fmt.Errorf("navigation failed: %w", err)
|
||||||
|
}
|
||||||
|
if err := currentPage.Timeout(timeout).WaitLoad(); err != nil {
|
||||||
|
return nil, fmt.Errorf("page load failed: %w", err)
|
||||||
|
}
|
||||||
|
return t.pageContent(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *BrowserTool) click(selector string, timeout time.Duration) ([]byte, error) {
|
||||||
|
if err := ensurePage(timeout); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
el, err := currentPage.Timeout(timeout).Element(selector)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("element %q not found: %w", selector, err)
|
||||||
|
}
|
||||||
|
if err := el.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||||
|
return nil, fmt.Errorf("click failed: %w", err)
|
||||||
|
}
|
||||||
|
if err := currentPage.WaitIdle(timeout); err != nil {
|
||||||
|
return nil, fmt.Errorf("wait after click failed: %w", err)
|
||||||
|
}
|
||||||
|
return t.pageContent(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *BrowserTool) fill(selector, value string, timeout time.Duration) ([]byte, error) {
|
||||||
|
if err := ensurePage(timeout); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
el, err := currentPage.Timeout(timeout).Element(selector)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("element %q not found: %w", selector, err)
|
||||||
|
}
|
||||||
|
if err := el.SelectAllText(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to select text: %w", err)
|
||||||
|
}
|
||||||
|
if err := el.Input(value); err != nil {
|
||||||
|
return nil, fmt.Errorf("fill failed: %w", err)
|
||||||
|
}
|
||||||
|
return t.pageContent(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *BrowserTool) pageContent(timeout time.Duration) ([]byte, error) {
|
||||||
|
if currentPage == nil {
|
||||||
|
return []byte("No page loaded."), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := currentPage.Info()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get page info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
titleRes, err := currentPage.Eval("() => document.title")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get page title: %w", err)
|
||||||
|
}
|
||||||
|
title := titleRes.Value.String()
|
||||||
|
|
||||||
|
body, err := currentPage.Timeout(timeout).Element("body")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get page body: %w", err)
|
||||||
|
}
|
||||||
|
text, err := body.Text()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract page text: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
fmt.Fprintf(&sb, "Page: %s\nURL: %s\n\n%s", title, info.URL, text)
|
||||||
|
return []byte(sb.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *BrowserTool) pageLinks(timeout time.Duration) ([]byte, error) {
|
||||||
|
if currentPage == nil {
|
||||||
|
return []byte("No page loaded."), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
elements, err := currentPage.Timeout(timeout).Elements("a[href]")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get links: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
fmt.Fprintf(&sb, "Links on current page (%d):\n\n", len(elements))
|
||||||
|
for i, el := range elements {
|
||||||
|
href, _ := el.Attribute("href")
|
||||||
|
text, _ := el.Text()
|
||||||
|
if href == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
label := strings.TrimSpace(text)
|
||||||
|
if label == "" {
|
||||||
|
label = "(no text)"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&sb, "%d. %s — %s\n", i+1, label, *href)
|
||||||
|
}
|
||||||
|
return []byte(sb.String()), nil
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ type Chat struct {
|
|||||||
UserID string `db:"user_id" json:"userId"`
|
UserID string `db:"user_id" json:"userId"`
|
||||||
TaskID string `db:"task_id" json:"taskId,omitempty"`
|
TaskID string `db:"task_id" json:"taskId,omitempty"`
|
||||||
Title string `db:"title" json:"title"`
|
Title string `db:"title" json:"title"`
|
||||||
|
FreshTitle bool `db:"fresh_title" json:"freshTitle"`
|
||||||
DateCreated time.Time `db:"_date_created" json:"_date_created"`
|
DateCreated time.Time `db:"_date_created" json:"_date_created"`
|
||||||
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
|
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
|
||||||
VersionId string `db:"_version" json:"_version"`
|
VersionId string `db:"_version" json:"_version"`
|
||||||
@@ -139,10 +140,17 @@ type LLMEngine interface {
|
|||||||
params *LLMParams,
|
params *LLMParams,
|
||||||
messages []*Message,
|
messages []*Message,
|
||||||
) *Message
|
) *Message
|
||||||
|
Generate(
|
||||||
|
ctx context.Context,
|
||||||
|
provider *Provider,
|
||||||
|
modelID string,
|
||||||
|
messages []*Message,
|
||||||
|
) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatRepository interface {
|
type ChatRepository interface {
|
||||||
ListChats(userID string, teamID string, page int) ([]*Chat, error)
|
ListChats(userID string, teamID string, page int) ([]*Chat, error)
|
||||||
|
ListChatsWithUnfreshTitles(teamID string) ([]*Chat, error)
|
||||||
GetChat(userID string, teamID string, id string) (*Chat, error)
|
GetChat(userID string, teamID string, id string) (*Chat, error)
|
||||||
CreateChat(userID string, chat *Chat) (*Chat, error)
|
CreateChat(userID string, chat *Chat) (*Chat, error)
|
||||||
UpdateChat(userID string, chat *Chat) (*Chat, error)
|
UpdateChat(userID string, chat *Chat) (*Chat, error)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -61,6 +62,9 @@ func (h *ChatHandler) NewChat(c *echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ChatHandler) NewChatMessage(c *echo.Context) error {
|
func (h *ChatHandler) NewChatMessage(c *echo.Context) error {
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
userID := c.Get(auth.ContextUserIDKey).(string)
|
userID := c.Get(auth.ContextUserIDKey).(string)
|
||||||
teamID := c.Param("space")
|
teamID := c.Param("space")
|
||||||
chatID := c.Param("id")
|
chatID := c.Param("id")
|
||||||
@@ -75,7 +79,7 @@ func (h *ChatHandler) NewChatMessage(c *echo.Context) error {
|
|||||||
chunkChan := make(chan *domain.Chunk)
|
chunkChan := make(chan *domain.Chunk)
|
||||||
|
|
||||||
_, err = h.chatService.AddChatMessage(
|
_, err = h.chatService.AddChatMessage(
|
||||||
c.Request().Context(),
|
ctx,
|
||||||
userID,
|
userID,
|
||||||
teamID,
|
teamID,
|
||||||
chatID,
|
chatID,
|
||||||
|
|||||||
@@ -146,6 +146,12 @@ func (s *Service) AddChatMessage(
|
|||||||
cb,
|
cb,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
chat, _ := s.repoChat.GetChat(userID, teamID, chatID)
|
||||||
|
chat.FreshTitle = false
|
||||||
|
s.repoChat.UpdateChat(userID, chat)
|
||||||
|
|
||||||
|
go s.RefreshTitles(provider, chatModelID, teamID)
|
||||||
|
|
||||||
return message, nil
|
return message, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package chat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) RefreshTitles(provider *domain.Provider, modelID string, teamID string) {
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
chats, err := s.repoChat.ListChatsWithUnfreshTitles(teamID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("RefreshTitles.ListChats", "error", err)
|
||||||
|
}
|
||||||
|
for _, chat := range chats {
|
||||||
|
newchat, _ := s.repoChat.GetChat(chat.UserID, chat.TeamID, chat.ID)
|
||||||
|
|
||||||
|
if !chat.FreshTitle {
|
||||||
|
messages, err := s.repoChat.GetChatMessages(newchat.UserID, newchat.TeamID, newchat.ID, true)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("RefreshTitles.GetMessages", "error", err)
|
||||||
|
}
|
||||||
|
title, err := s.llmEngine.Generate(ctx, provider, modelID, append(messages, &domain.Message{
|
||||||
|
Role: string(domain.RoleUser),
|
||||||
|
Content: "resume toute la conversation dans un titre entre 5 et 10 mots",
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("RefreshTitles.Generate", "error", err)
|
||||||
|
}
|
||||||
|
newchat.FreshTitle = true
|
||||||
|
newchat.Title = title
|
||||||
|
s.repoChat.UpdateChat(newchat.UserID, newchat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -1,24 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {autofocus} from "$lib/ui/autofocus"
|
import {autofocus} from "$lib/ui/autofocus"
|
||||||
import {onMount, tick } from "svelte"
|
import {onMount, tick, untrack } 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';
|
||||||
import type { Message, ChatWithMessages } from "$lib/types/api"
|
import type { Message } from "$lib/types/api"
|
||||||
import SvelteMarkdown from '@humanspeak/svelte-markdown'
|
import SvelteMarkdown from '@humanspeak/svelte-markdown'
|
||||||
|
|
||||||
let prompt = $state("")
|
let prompt = $state("")
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
let messages = $state([] as Message[])
|
let messages = $state(untrack(()=>data.chatWithMessage.messages))
|
||||||
let waiting = $state(false)
|
let waiting = $state(false)
|
||||||
|
|
||||||
onMount(async ()=>{
|
onMount(async ()=>{
|
||||||
if (page.state.prompt) {
|
if (page.state.prompt) {
|
||||||
console.log(page.state.prompt)
|
console.log(page.state.prompt)
|
||||||
await sendRequest(page.state.prompt)
|
await sendRequest(page.state.prompt)
|
||||||
} else {
|
|
||||||
let chatWithMessage : ChatWithMessages = await chatApi.getChat(data.spaceId, data.chatId)
|
|
||||||
messages = chatWithMessage.messages
|
|
||||||
}
|
}
|
||||||
await tick()
|
await tick()
|
||||||
historyDiv.scrollTop = historyDiv.scrollHeight;
|
historyDiv.scrollTop = historyDiv.scrollHeight;
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
import { chatApi } from "$lib/api/endpoints/chat"
|
||||||
|
|
||||||
export const load: PageLoad = ({params}) => {
|
export const load: PageLoad = async ({ params }) => {
|
||||||
|
const chatWithMessage = await chatApi.getChat(params.space, params.conversation)
|
||||||
return {
|
return {
|
||||||
title: 'Nouvelle conversation',
|
title: chatWithMessage.chat.title,
|
||||||
spaceId: params.space,
|
spaceId: params.space,
|
||||||
chatId: params.conversation,
|
chatId: params.conversation,
|
||||||
|
chatWithMessage: chatWithMessage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -21,11 +21,11 @@ let {
|
|||||||
|
|
||||||
let tool = $state(untrack(()=>item))
|
let tool = $state(untrack(()=>item))
|
||||||
let toolParams = $state([] as ToolParam[])
|
let toolParams = $state([] as ToolParam[])
|
||||||
let paramValues = new SvelteMap<string, string>();
|
let paramValues = new SvelteMap<string, unknown>();
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
toolParams = await toolApi.getToolParams(item.teamId, item.id)
|
toolParams = await toolApi.getToolParams(item.teamId, item.id)
|
||||||
paramValues = new SvelteMap<string, string>(Object.entries(JSON.parse(item.configuration)));
|
paramValues = new SvelteMap<string, unknown>(Object.entries(JSON.parse(item.configuration)));
|
||||||
})
|
})
|
||||||
|
|
||||||
const autofocus = (node: HTMLElement) => {
|
const autofocus = (node: HTMLElement) => {
|
||||||
@@ -79,12 +79,29 @@ const handleCancel = (e: Event) => {
|
|||||||
|
|
||||||
<div class="formelement">
|
<div class="formelement">
|
||||||
<label for={param.name}>{param.name}</label>
|
<label for={param.name}>{param.name}</label>
|
||||||
|
{#if param.type == "bool"}
|
||||||
|
<input
|
||||||
|
id="enabled"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={() => paramValues.get(param.name) as boolean ?? '', (v) => paramValues.set(param.name, v)}
|
||||||
|
class="checkboxinput"
|
||||||
|
/>
|
||||||
|
{:else if param.type=="int"}
|
||||||
|
<input
|
||||||
|
id={param.name}
|
||||||
|
type="number"
|
||||||
|
bind:value={() => paramValues.get(param.name) ?? '', (v) => paramValues.set(param.name, v)}
|
||||||
|
class="textinput"
|
||||||
|
placeholder={param.description}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
<input
|
<input
|
||||||
id={param.name}
|
id={param.name}
|
||||||
bind:value={() => paramValues.get(param.name) ?? '', (v) => paramValues.set(param.name, v)}
|
bind:value={() => paramValues.get(param.name) ?? '', (v) => paramValues.set(param.name, v)}
|
||||||
class="textinput"
|
class="textinput"
|
||||||
placeholder={param.description}
|
placeholder={param.description}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
<FormError key="baseUrl" errors={errors} />
|
<FormError key="baseUrl" errors={errors} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user