Ajout de SEARXNG pour les recherches web

This commit is contained in:
fabien committed 2026-10-05 19:05:24 +02:00
1 parent 87eeb41364
commit 345129c7bf
30 files changed
+919 -166

No files matched your search

@@ -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,
)
}
@@ -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"
}
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"`
+1
View File
@@ -29,6 +29,7 @@ type Config struct {
StorageS3AccessKeySecret string // S3
StorageS3UseSSL string // S3
ServerID string
SearXngUrl string
}
var config *Config
+10 -2
View File
@@ -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 {
+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 (
"context"
@@ -1,4 +1,4 @@
package brain
package gateway
type Session struct {
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/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)
+5
View File
@@ -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,
}
+19 -3
View File
@@ -3,9 +3,11 @@ 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
@@ -20,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
@@ -30,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
@@ -76,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{
-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]
}