diff --git a/backend/cmd/serve.go b/backend/cmd/serve.go index 0ad6614..159ce21 100644 --- a/backend/cmd/serve.go +++ b/backend/cmd/serve.go @@ -1,7 +1,13 @@ package cmd import ( + "context" + "errors" "log/slog" + nethttp "net/http" + "os" + "os/signal" + "syscall" "time" "github.com/spf13/cobra" @@ -11,6 +17,7 @@ import ( "trankilou.fr/lassistanoque/backend/internal/adapter/llm" "trankilou.fr/lassistanoque/backend/internal/adapter/security" "trankilou.fr/lassistanoque/backend/internal/http" + "trankilou.fr/lassistanoque/backend/internal/llmserver" "trankilou.fr/lassistanoque/backend/internal/service/agent" "trankilou.fr/lassistanoque/backend/internal/service/auth" "trankilou.fr/lassistanoque/backend/internal/service/provider" @@ -84,7 +91,41 @@ func runServe() { AgentService: agentService, }) - if err := router.Start(); err != nil { - slog.Error("failed to start server", "error", err) + llms := llmserver.NewLLMServer(llmserver.Dependencies{}) + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + // start LLM service + go func() { + if err := llms.Start(); err != nil && !errors.Is(err, nethttp.ErrServerClosed) { + slog.Error("failed to start llm server", "error", err) + } + }() + + // start HTTP service + go func() { + if err := router.Start(); err != nil && !errors.Is(err, nethttp.ErrServerClosed) { + slog.Error("failed to start http server", "error", err) + } + }() + + <-ctx.Done() + + // stop HTTP service + ctx, cancel1 := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel1() + if err := router.Stop(ctx); err != nil { + slog.Error("failed to stop http server", "error", err) } + slog.Info("HTTP server stopped gracefully") + + // stop LLM service + ctx, cancel2 := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel2() + if err := llms.Stop(ctx); err != nil { + slog.Error("failed to stop llm server", "error", err) + } + slog.Info("LLM server stopped gracefully") + } diff --git a/backend/internal/adapter/database/turso/migrations/000001_create_tables.up.sql b/backend/internal/adapter/database/turso/migrations/000001_create_tables.up.sql index 73f1605..9abab00 100644 --- a/backend/internal/adapter/database/turso/migrations/000001_create_tables.up.sql +++ b/backend/internal/adapter/database/turso/migrations/000001_create_tables.up.sql @@ -63,7 +63,7 @@ create table user_addresses ( create table teams ( id text not null primary key, label text not null, - default_model_id text, + default_agent_id text, _date_created numeric not null default current_timestamp, _date_updated numeric , _version text not null diff --git a/backend/internal/adapter/database/turso/user_repository.go b/backend/internal/adapter/database/turso/user_repository.go index c5a8149..bda158f 100644 --- a/backend/internal/adapter/database/turso/user_repository.go +++ b/backend/internal/adapter/database/turso/user_repository.go @@ -68,7 +68,7 @@ func (ur *TursoUserRepository) DeleteUser(id string) error { } func (ur *TursoUserRepository) FindTeam(userid string, teamid string) (*domain.Team, error) { - return ur.TeamTable.SelectOne("id=$1 and user_id=$2", teamid, userid) + return ur.TeamTable.SelectOne("id=$1 and id in (select team_id from user_teams where user_id=$2)", teamid, userid) } func (ur *TursoUserRepository) ListTeams(userid string) ([]*domain.Team, error) { diff --git a/backend/internal/adapter/llm/llm.go b/backend/internal/adapter/llm/anyllm.go similarity index 67% rename from backend/internal/adapter/llm/llm.go rename to backend/internal/adapter/llm/anyllm.go index 031244d..e1624a2 100644 --- a/backend/internal/adapter/llm/llm.go +++ b/backend/internal/adapter/llm/anyllm.go @@ -74,3 +74,48 @@ func (e *AnyLLMEngine) ListModelsFromProvider(ctx context.Context, provider *dom } return models, nil } + +func (e *AnyLLMEngine) Stream( + ctx context.Context, + provider *domain.Provider, + modelID string, + params *domain.StreamParams, + messages []*domain.Message, +) 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, + }) + } + + chunkChan, errChan := p.CompletionStream(ctx, anyllm.CompletionParams{ + Model: modelID, + Messages: anyllmMessages, + Stream: true, + }) + + for chunk := range chunkChan { + if len(chunk.Choices) > 0 { + content := chunk.Choices[0].Delta.Content + if params.OnChunk != nil { + params.OnChunk(&domain.Chunk{ + Content: content, + }) + } + } + } + + if err := <-errChan; err != nil { + return err + } + + return nil +} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index cfd8a74..3360cf2 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -13,6 +13,7 @@ const ( DEFAULT_DB_URL = "lassistanoque.db" DEFAULT_HTTP_PORT = 3000 DEFAULT_STORAGE_TYPE = "database" + DEFAULT_SERVER_ID = "default" ) type Config struct { @@ -27,6 +28,7 @@ type Config struct { StorageS3AccessKeyID string // S3 StorageS3AccessKeySecret string // S3 StorageS3UseSSL string // S3 + ServerID string } var config *Config @@ -56,6 +58,10 @@ func GetConfig() *Config { if storageType == "" { storageType = DEFAULT_STORAGE_TYPE } + serverID := os.Getenv("LASSISTANOQUE_SERVER_ID") + if serverID == "" { + serverID = DEFAULT_SERVER_ID + } config = &Config{ DatabaseType: databaseType, @@ -63,6 +69,7 @@ func GetConfig() *Config { HttpPort: httpPort, JWTSecret: jwtsecret, StorageType: storageType, + ServerID: serverID, } } diff --git a/backend/internal/domain/llm.go b/backend/internal/domain/llm.go index 8561108..09587ed 100644 --- a/backend/internal/domain/llm.go +++ b/backend/internal/domain/llm.go @@ -20,7 +20,48 @@ type LLMModel struct { Detail *LLMModelDetail `json:"detail"` } +type Message struct { + Role string + Content string + ToolCalls []ToolCall + ToolCallID string + + //Reasoning *Reasoning +} + +type Tool struct { +} + +type ToolCall struct { +} + +type Chunk struct { + Content string +} + +type StreamParams struct { + Tools []*Tool + OnChunk func(chunk *Chunk) + OnDebug func() +} + +type Role string + +const ( + RoleSystem Role = "system" + RoleUser Role = "user" + RoleAssistant Role = "assistant" + RoleTool Role = "tool" +) + type LLMEngine interface { ListProviderTypes() []Item ListModelsFromProvider(ctx context.Context, provider *Provider) ([]string, error) + Stream( + ctx context.Context, + provider *Provider, + modelID string, + params *StreamParams, + messages []*Message, + ) error } diff --git a/backend/internal/domain/user.go b/backend/internal/domain/user.go index 2cb7933..3b94dd5 100644 --- a/backend/internal/domain/user.go +++ b/backend/internal/domain/user.go @@ -23,7 +23,7 @@ type User struct { type Team struct { ID string `db:"id" json:"id"` Label string `db:"label" json:"label"` - DefaultModelID *string `db:"default_model_id" json:"defaultModelId"` + DefaultAgentID *string `db:"default_agent_id" json:"defaultAgentId"` DateCreated time.Time `db:"_date_created" json:"_dateCreated"` DateUpdated *time.Time `db:"_date_updated" json:"_dateUpdated"` VersionId string `db:"_version" json:"_version"` diff --git a/backend/internal/http/handlers/agent.go b/backend/internal/http/handlers/agent.go index 3f8da44..e0fc9ab 100644 --- a/backend/internal/http/handlers/agent.go +++ b/backend/internal/http/handlers/agent.go @@ -7,11 +7,19 @@ import ( "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/user" ) -func NewAgentGroup(prefix string, e *echo.Group, service *agent.Service, middlewares ...echo.MiddlewareFunc) *echo.Group { +func NewAgentGroup( + prefix string, + e *echo.Group, + agentService *agent.Service, + userService *user.Service, + middlewares ...echo.MiddlewareFunc) *echo.Group { + agentHandler := &AgentHandler{ - agentService: service, + agentService: agentService, + userService: userService, } agent := e.Group(prefix, middlewares...) @@ -26,6 +34,7 @@ func NewAgentGroup(prefix string, e *echo.Group, service *agent.Service, middlew type AgentHandler struct { agentService *agent.Service + userService *user.Service } func (h *AgentHandler) ListAgents(c *echo.Context) error { @@ -98,3 +107,11 @@ func (h *AgentHandler) DeleteAgent(c *echo.Context) error { } return c.JSON(http.StatusOK, agentID) } + +type DefaulAgentRequest struct { + AgentID string `json:"AgentId"` +} +type DefaulAgentResponse struct { + NewDefaultAgentID *string `json:"newDefaultAgentId"` + OldDefaultAgentID *string `json:"oldDefaultAgentId"` +} diff --git a/backend/internal/http/handlers/team.go b/backend/internal/http/handlers/team.go new file mode 100644 index 0000000..57fe2a7 --- /dev/null +++ b/backend/internal/http/handlers/team.go @@ -0,0 +1,55 @@ +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/user" +) + +func NewTeamGroup(prefix string, e *echo.Group, service *user.Service, middlewares ...echo.MiddlewareFunc) *echo.Group { + teamHandler := &TeamHandler{ + userService: service, + } + h := e.Group(prefix, middlewares...) + h.GET("/:team", teamHandler.Get) + h.PUT("/:team", teamHandler.Update) + return h +} + +type TeamHandler struct { + userService *user.Service +} + +func (h TeamHandler) Get(c *echo.Context) error { + userID := c.Get(auth.ContextUserIDKey).(string) + teamID := c.Param("team") + user, err := h.userService.GetTeam(userID, teamID) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + return c.JSON(http.StatusOK, user) +} + +func (h TeamHandler) Update(c *echo.Context) error { + userID := c.Get(auth.ContextUserIDKey).(string) + teamID := c.Param("team") + + var updTeam domain.Team + if err := c.Bind(&updTeam); err != nil { + return c.String(http.StatusBadRequest, "bad request") + } + + if teamID != updTeam.ID { + return c.String(http.StatusBadRequest, "bad request") + } + + user, err := h.userService.UpdateTeam(userID, &updTeam) + if err != nil { + return c.String(http.StatusBadRequest, err.Error()) + } + + return c.JSON(http.StatusOK, user) +} diff --git a/backend/internal/http/router.go b/backend/internal/http/router.go index 768bdae..e6c70e7 100644 --- a/backend/internal/http/router.go +++ b/backend/internal/http/router.go @@ -1,6 +1,7 @@ package http import ( + "context" "embed" "fmt" "io/fs" @@ -30,7 +31,7 @@ var indexhtml []byte var robotstxt []byte type Router struct { - echo *echo.Echo + server *http.Server } type Dependencies struct { @@ -44,6 +45,8 @@ type Dependencies struct { func NewRouter(deps Dependencies) *Router { + cfg := config.GetConfig() + e := echo.New() e.Use(middleware.RequestLogger()) e.Use(middleware.CORS("http://localhost:5173")) @@ -76,14 +79,23 @@ func NewRouter(deps Dependencies) *Router { _ = handlers.NewUserGroup("/user", api, deps.UserService, deps.TokenManager.TokenMiddleware) _ = handlers.NewMiscGroup("/misc", api, deps.TokenManager.TokenMiddleware) _ = handlers.NewModelGroup("/provider", api, deps.ProviderService, deps.TokenManager.TokenMiddleware) - _ = handlers.NewAgentGroup("/agent", api, deps.AgentService, deps.TokenManager.TokenMiddleware) + _ = handlers.NewAgentGroup("/agent", api, deps.AgentService, deps.UserService, deps.TokenManager.TokenMiddleware) + _ = handlers.NewTeamGroup("/team", api, deps.UserService, deps.TokenManager.TokenMiddleware) + + server := http.Server{ + Addr: fmt.Sprintf(":%d", cfg.HttpPort), + Handler: e, + } return &Router{ - echo: e, + &server, } } func (r *Router) Start() error { - cfg := config.GetConfig() - return r.echo.Start(fmt.Sprintf(":%d", cfg.HttpPort)) + return r.server.ListenAndServe() +} + +func (r *Router) Stop(ctx context.Context) error { + return r.server.Shutdown(ctx) } diff --git a/backend/internal/llmserver/server.go b/backend/internal/llmserver/server.go new file mode 100644 index 0000000..45ed046 --- /dev/null +++ b/backend/internal/llmserver/server.go @@ -0,0 +1,58 @@ +package llmserver + +import ( + "context" + "fmt" + "sync" + + "trankilou.fr/lassistanoque/backend/internal/utility" +) + +type Dependencies struct { +} + +type LLMServer struct { + started bool + mu sync.Mutex + sessions map[string]*Session // running sessions + newSessionChan chan string + closeChan chan bool +} + +func NewLLMServer(deps Dependencies) *LLMServer { + return &LLMServer{ + started: true, + sessions: make(map[string]*Session), + newSessionChan: make(chan string), + closeChan: make(chan bool), + } +} + +func (s *LLMServer) Start() error { + + for { + select { + case _ = <-s.closeChan: + fmt.Println("Closing LLM Server") + return nil + } + } + +} + +func (s *LLMServer) Stop(ctx context.Context) error { + s.closeChan <- true + s.started = false + return nil +} + +func (s *LLMServer) StartSession() (*Session, error) { + sid := utility.GenID() + session := &Session{} + + s.mu.Lock() + s.sessions[sid] = session + s.mu.Unlock() + + return session, nil +} diff --git a/backend/internal/llmserver/session.go b/backend/internal/llmserver/session.go new file mode 100644 index 0000000..4e9f363 --- /dev/null +++ b/backend/internal/llmserver/session.go @@ -0,0 +1,5 @@ +package llmserver + +type Session struct { + server string +} diff --git a/backend/internal/service/user/service.go b/backend/internal/service/user/service.go index 67cfb5c..e8d797b 100644 --- a/backend/internal/service/user/service.go +++ b/backend/internal/service/user/service.go @@ -105,3 +105,24 @@ func (s *Service) DeleteAddress(userId string, addressId string) error { } return s.repo.DeleteUserAddress(addressId) } + +func (s *Service) GetTeam(userid, id string) (*domain.Team, error) { + team, err := s.repo.FindTeam(userid, id) + if err != nil { + return nil, err + } + return team, nil +} + +func (s *Service) UpdateTeam(userid string, team *domain.Team) (*domain.Team, error) { + updTeam, err := s.repo.FindTeam(userid, team.ID) + if err != nil { + return nil, err + } + + updTeam.Label = team.Label + updTeam.DefaultAgentID = team.DefaultAgentID + + return s.repo.UpdateTeam(userid, updTeam) + +} diff --git a/backend/lassistanoque.db-wal b/backend/lassistanoque.db-wal index bba7ca4..57f39fc 100644 Binary files a/backend/lassistanoque.db-wal and b/backend/lassistanoque.db-wal differ diff --git a/backend/web/src/lib/api/endpoints/agent.ts b/backend/web/src/lib/api/endpoints/agent.ts index 9743bcc..23968dc 100644 --- a/backend/web/src/lib/api/endpoints/agent.ts +++ b/backend/web/src/lib/api/endpoints/agent.ts @@ -6,5 +6,5 @@ export const agentApi = { getAgent: (space: string, id: string) => api.get(`/agent/${space}/${id}`), createAgent: (space: string,data: Partial) => api.post(`/agent/${space}`, data), updateAgent: (space: string,data: Partial) => api.put(`/agent/${space}`, data), - deleteAgent: (space: string,id: string) => api.delete(`/agent/${space}/${id}`), + deleteAgent: (space: string, id: string) => api.delete(`/agent/${space}/${id}`), }; diff --git a/backend/web/src/lib/api/endpoints/team.ts b/backend/web/src/lib/api/endpoints/team.ts new file mode 100644 index 0000000..4bf87a3 --- /dev/null +++ b/backend/web/src/lib/api/endpoints/team.ts @@ -0,0 +1,7 @@ +import { api } from '../client'; +import type { Team } from "$lib/types/api" + +export const teamApi = { + getSpace: (space: string) => api.get(`/team/${space}`), + updateSpace: (space: string,data: Partial) => api.put(`/team/${space}`, data), +}; diff --git a/backend/web/src/lib/components/Menu.svelte b/backend/web/src/lib/components/Menu.svelte index 80e208b..e9876ee 100644 --- a/backend/web/src/lib/components/Menu.svelte +++ b/backend/web/src/lib/components/Menu.svelte @@ -1,10 +1,18 @@ {/if}