mirror of
https://github.com/mudler/LocalAI.git
synced 2026-03-31 13:15:51 -04:00
feat: add agentic management (#8820)
* feat: add standalone and agentic functionalities Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * expose agents via responses api Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
committed by
GitHub
parent
e1df6807dc
commit
ac48867b7d
6
.github/gallery-agent/agent.go
vendored
6
.github/gallery-agent/agent.go
vendored
@@ -13,8 +13,8 @@ import (
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
hfapi "github.com/mudler/LocalAI/pkg/huggingface-api"
|
||||
cogito "github.com/mudler/cogito"
|
||||
|
||||
"github.com/mudler/cogito"
|
||||
"github.com/mudler/cogito/clients"
|
||||
"github.com/mudler/cogito/structures"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
@@ -25,7 +25,7 @@ var (
|
||||
openAIBaseURL = os.Getenv("OPENAI_BASE_URL")
|
||||
galleryIndexPath = os.Getenv("GALLERY_INDEX_PATH")
|
||||
//defaultclient
|
||||
llm = cogito.NewOpenAILLM(openAIModel, openAIKey, openAIBaseURL)
|
||||
llm = clients.NewOpenAILLM(openAIModel, openAIKey, openAIBaseURL)
|
||||
)
|
||||
|
||||
// cleanTextContent removes trailing spaces, tabs, and normalizes line endings
|
||||
|
||||
@@ -194,6 +194,7 @@ local-ai run oci://localai/phi-2:latest
|
||||
For more information, see [💻 Getting started](https://localai.io/basics/getting_started/index.html), if you are interested in our roadmap items and future enhancements, you can see the [Issues labeled as Roadmap here](https://github.com/mudler/LocalAI/issues?q=is%3Aissue+is%3Aopen+label%3Aroadmap)
|
||||
|
||||
## 📰 Latest project news
|
||||
- March 2026: [Agent management](https://github.com/mudler/LocalAI/pull/8820), [New React UI](https://github.com/mudler/LocalAI/pull/8772), [WebRTC](https://github.com/mudler/LocalAI/pull/8790),[MLX-distributed via P2P and RDMA](https://github.com/mudler/LocalAI/pull/8801)
|
||||
- February 2026: [Realtime API for audio-to-audio with tool calling](https://github.com/mudler/LocalAI/pull/6245), [ACE-Step 1.5 support](https://github.com/mudler/LocalAI/pull/8396)
|
||||
- January 2026: **LocalAI 3.10.0** - Major release with Anthropic API support, Open Responses API for stateful agents, video & image generation suite (LTX-2), unified GPU backends, tool streaming & XML parsing, system-aware backend gallery, crash fixes for AVX-only CPUs and AMD VRAM reporting, request tracing, and new backends: **Moonshine** (ultra-fast transcription), **Pocket-TTS** (lightweight TTS). Vulkan arm64 builds now available. [Release notes](https://github.com/mudler/LocalAI/releases/tag/v3.10.0).
|
||||
- December 2025: [Dynamic Memory Resource reclaimer](https://github.com/mudler/LocalAI/pull/7583), [Automatic fitting of models to multiple GPUS(llama.cpp)](https://github.com/mudler/LocalAI/pull/7584), [Added Vibevoice backend](https://github.com/mudler/LocalAI/pull/7494)
|
||||
@@ -240,6 +241,7 @@ Roadmap items: [List of issues](https://github.com/mudler/LocalAI/issues?q=is%3A
|
||||
- 📈 [Reranker API](https://localai.io/features/reranker/)
|
||||
- 🆕🖧 [P2P Inferencing](https://localai.io/features/distribute/)
|
||||
- 🆕🔌 [Model Context Protocol (MCP)](https://localai.io/docs/features/mcp/) - Agentic capabilities with external tools and [LocalAGI's Agentic capabilities](https://github.com/mudler/LocalAGI)
|
||||
- 🆕🤖 [Built-in Agents](https://localai.io/features/agents/) - Autonomous AI agents with tool use, knowledge base (RAG), skills, SSE streaming, import/export, and [Agent Hub](https://agenthub.localai.io) — powered by [LocalAGI](https://github.com/mudler/LocalAGI)
|
||||
- 🔊 Voice activity detection (Silero-VAD support)
|
||||
- 🌍 Integrated WebUI!
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/mudler/LocalAI/core/services"
|
||||
"github.com/mudler/LocalAI/core/templates"
|
||||
"github.com/mudler/LocalAI/pkg/model"
|
||||
"github.com/mudler/xlog"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
@@ -18,6 +19,7 @@ type Application struct {
|
||||
templatesEvaluator *templates.Evaluator
|
||||
galleryService *services.GalleryService
|
||||
agentJobService *services.AgentJobService
|
||||
agentPoolService *services.AgentPoolService
|
||||
watchdogMutex sync.Mutex
|
||||
watchdogStop chan bool
|
||||
p2pMutex sync.Mutex
|
||||
@@ -59,6 +61,10 @@ func (a *Application) AgentJobService() *services.AgentJobService {
|
||||
return a.agentJobService
|
||||
}
|
||||
|
||||
func (a *Application) AgentPoolService() *services.AgentPoolService {
|
||||
return a.agentPoolService
|
||||
}
|
||||
|
||||
// StartupConfig returns the original startup configuration (from env vars, before file loading)
|
||||
func (a *Application) StartupConfig() *config.ApplicationConfig {
|
||||
return a.startupConfig
|
||||
@@ -88,5 +94,19 @@ func (a *Application) start() error {
|
||||
|
||||
a.agentJobService = agentJobService
|
||||
|
||||
// Initialize agent pool service (LocalAGI integration)
|
||||
if a.applicationConfig.AgentPool.Enabled {
|
||||
aps, err := services.NewAgentPoolService(a.applicationConfig)
|
||||
if err == nil {
|
||||
if err := aps.Start(a.applicationConfig.Context); err != nil {
|
||||
xlog.Error("Failed to start agent pool", "error", err)
|
||||
} else {
|
||||
a.agentPoolService = aps
|
||||
}
|
||||
} else {
|
||||
xlog.Error("Failed to create agent pool service", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -87,6 +87,28 @@ type RunCMD struct {
|
||||
AgentJobRetentionDays int `env:"LOCALAI_AGENT_JOB_RETENTION_DAYS,AGENT_JOB_RETENTION_DAYS" default:"30" help:"Number of days to keep agent job history (default: 30)" group:"api"`
|
||||
OpenResponsesStoreTTL string `env:"LOCALAI_OPEN_RESPONSES_STORE_TTL,OPEN_RESPONSES_STORE_TTL" default:"0" help:"TTL for Open Responses store (e.g., 1h, 30m, 0 = no expiration)" group:"api"`
|
||||
|
||||
// Agent Pool (LocalAGI)
|
||||
DisableAgents bool `env:"LOCALAI_DISABLE_AGENTS" default:"false" help:"Disable the agent pool feature" group:"agents"`
|
||||
AgentPoolAPIURL string `env:"LOCALAI_AGENT_POOL_API_URL" help:"Default API URL for agents (defaults to self-referencing LocalAI)" group:"agents"`
|
||||
AgentPoolAPIKey string `env:"LOCALAI_AGENT_POOL_API_KEY" help:"Default API key for agents (defaults to first LocalAI API key)" group:"agents"`
|
||||
AgentPoolDefaultModel string `env:"LOCALAI_AGENT_POOL_DEFAULT_MODEL" help:"Default model for agents" group:"agents"`
|
||||
AgentPoolMultimodalModel string `env:"LOCALAI_AGENT_POOL_MULTIMODAL_MODEL" help:"Default multimodal model for agents" group:"agents"`
|
||||
AgentPoolTranscriptionModel string `env:"LOCALAI_AGENT_POOL_TRANSCRIPTION_MODEL" help:"Default transcription model for agents" group:"agents"`
|
||||
AgentPoolTranscriptionLanguage string `env:"LOCALAI_AGENT_POOL_TRANSCRIPTION_LANGUAGE" help:"Default transcription language for agents" group:"agents"`
|
||||
AgentPoolTTSModel string `env:"LOCALAI_AGENT_POOL_TTS_MODEL" help:"Default TTS model for agents" group:"agents"`
|
||||
AgentPoolStateDir string `env:"LOCALAI_AGENT_POOL_STATE_DIR" help:"State directory for agent pool" group:"agents"`
|
||||
AgentPoolTimeout string `env:"LOCALAI_AGENT_POOL_TIMEOUT" default:"5m" help:"Default agent timeout" group:"agents"`
|
||||
AgentPoolEnableSkills bool `env:"LOCALAI_AGENT_POOL_ENABLE_SKILLS" default:"false" help:"Enable skills service for agents" group:"agents"`
|
||||
AgentPoolVectorEngine string `env:"LOCALAI_AGENT_POOL_VECTOR_ENGINE" default:"chromem" help:"Vector engine type for agent knowledge base" group:"agents"`
|
||||
AgentPoolEmbeddingModel string `env:"LOCALAI_AGENT_POOL_EMBEDDING_MODEL" default:"granite-embedding-107m-multilingual" help:"Embedding model for agent knowledge base" group:"agents"`
|
||||
AgentPoolCustomActionsDir string `env:"LOCALAI_AGENT_POOL_CUSTOM_ACTIONS_DIR" help:"Custom actions directory for agents" group:"agents"`
|
||||
AgentPoolDatabaseURL string `env:"LOCALAI_AGENT_POOL_DATABASE_URL" help:"Database URL for agent collections" group:"agents"`
|
||||
AgentPoolMaxChunkingSize int `env:"LOCALAI_AGENT_POOL_MAX_CHUNKING_SIZE" default:"400" help:"Maximum chunking size for knowledge base documents" group:"agents"`
|
||||
AgentPoolChunkOverlap int `env:"LOCALAI_AGENT_POOL_CHUNK_OVERLAP" default:"0" help:"Chunk overlap size for knowledge base documents" group:"agents"`
|
||||
AgentPoolEnableLogs bool `env:"LOCALAI_AGENT_POOL_ENABLE_LOGS" default:"false" help:"Enable agent logging" group:"agents"`
|
||||
AgentPoolCollectionDBPath string `env:"LOCALAI_AGENT_POOL_COLLECTION_DB_PATH" help:"Database path for agent collections" group:"agents"`
|
||||
AgentHubURL string `env:"LOCALAI_AGENT_HUB_URL" default:"https://agenthub.localai.io" help:"URL for the agent hub where users can browse and download agent configurations" group:"agents"`
|
||||
|
||||
Version bool
|
||||
}
|
||||
|
||||
@@ -203,6 +225,68 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error {
|
||||
opts = append(opts, config.DisableMCP)
|
||||
}
|
||||
|
||||
// Agent Pool
|
||||
if r.DisableAgents {
|
||||
opts = append(opts, config.DisableAgentPool)
|
||||
}
|
||||
if r.AgentPoolAPIURL != "" {
|
||||
opts = append(opts, config.WithAgentPoolAPIURL(r.AgentPoolAPIURL))
|
||||
}
|
||||
if r.AgentPoolAPIKey != "" {
|
||||
opts = append(opts, config.WithAgentPoolAPIKey(r.AgentPoolAPIKey))
|
||||
}
|
||||
if r.AgentPoolDefaultModel != "" {
|
||||
opts = append(opts, config.WithAgentPoolDefaultModel(r.AgentPoolDefaultModel))
|
||||
}
|
||||
if r.AgentPoolMultimodalModel != "" {
|
||||
opts = append(opts, config.WithAgentPoolMultimodalModel(r.AgentPoolMultimodalModel))
|
||||
}
|
||||
if r.AgentPoolTranscriptionModel != "" {
|
||||
opts = append(opts, config.WithAgentPoolTranscriptionModel(r.AgentPoolTranscriptionModel))
|
||||
}
|
||||
if r.AgentPoolTranscriptionLanguage != "" {
|
||||
opts = append(opts, config.WithAgentPoolTranscriptionLanguage(r.AgentPoolTranscriptionLanguage))
|
||||
}
|
||||
if r.AgentPoolTTSModel != "" {
|
||||
opts = append(opts, config.WithAgentPoolTTSModel(r.AgentPoolTTSModel))
|
||||
}
|
||||
if r.AgentPoolStateDir != "" {
|
||||
opts = append(opts, config.WithAgentPoolStateDir(r.AgentPoolStateDir))
|
||||
}
|
||||
if r.AgentPoolTimeout != "" {
|
||||
opts = append(opts, config.WithAgentPoolTimeout(r.AgentPoolTimeout))
|
||||
}
|
||||
if r.AgentPoolEnableSkills {
|
||||
opts = append(opts, config.EnableAgentPoolSkills)
|
||||
}
|
||||
if r.AgentPoolVectorEngine != "" {
|
||||
opts = append(opts, config.WithAgentPoolVectorEngine(r.AgentPoolVectorEngine))
|
||||
}
|
||||
if r.AgentPoolEmbeddingModel != "" {
|
||||
opts = append(opts, config.WithAgentPoolEmbeddingModel(r.AgentPoolEmbeddingModel))
|
||||
}
|
||||
if r.AgentPoolCustomActionsDir != "" {
|
||||
opts = append(opts, config.WithAgentPoolCustomActionsDir(r.AgentPoolCustomActionsDir))
|
||||
}
|
||||
if r.AgentPoolDatabaseURL != "" {
|
||||
opts = append(opts, config.WithAgentPoolDatabaseURL(r.AgentPoolDatabaseURL))
|
||||
}
|
||||
if r.AgentPoolMaxChunkingSize > 0 {
|
||||
opts = append(opts, config.WithAgentPoolMaxChunkingSize(r.AgentPoolMaxChunkingSize))
|
||||
}
|
||||
if r.AgentPoolChunkOverlap > 0 {
|
||||
opts = append(opts, config.WithAgentPoolChunkOverlap(r.AgentPoolChunkOverlap))
|
||||
}
|
||||
if r.AgentPoolEnableLogs {
|
||||
opts = append(opts, config.EnableAgentPoolLogs)
|
||||
}
|
||||
if r.AgentPoolCollectionDBPath != "" {
|
||||
opts = append(opts, config.WithAgentPoolCollectionDBPath(r.AgentPoolCollectionDBPath))
|
||||
}
|
||||
if r.AgentHubURL != "" {
|
||||
opts = append(opts, config.WithAgentHubURL(r.AgentHubURL))
|
||||
}
|
||||
|
||||
if idleWatchDog || busyWatchDog {
|
||||
opts = append(opts, config.EnableWatchDog)
|
||||
if idleWatchDog {
|
||||
|
||||
@@ -90,6 +90,33 @@ type ApplicationConfig struct {
|
||||
OpenResponsesStoreTTL time.Duration // TTL for Open Responses store (0 = no expiration)
|
||||
|
||||
PathWithoutAuth []string
|
||||
|
||||
// Agent Pool (LocalAGI integration)
|
||||
AgentPool AgentPoolConfig
|
||||
}
|
||||
|
||||
// AgentPoolConfig holds configuration for the LocalAGI agent pool integration.
|
||||
type AgentPoolConfig struct {
|
||||
Enabled bool // default: true (disabled by LOCALAI_DISABLE_AGENTS=true)
|
||||
StateDir string // default: DynamicConfigsDir (LocalAI configuration folder)
|
||||
APIURL string // default: self-referencing LocalAI (http://127.0.0.1:<port>)
|
||||
APIKey string // default: first API key from LocalAI config
|
||||
DefaultModel string
|
||||
MultimodalModel string
|
||||
TranscriptionModel string
|
||||
TranscriptionLanguage string
|
||||
TTSModel string
|
||||
Timeout string // default: "5m"
|
||||
EnableSkills bool
|
||||
EnableLogs bool
|
||||
CustomActionsDir string
|
||||
CollectionDBPath string
|
||||
VectorEngine string // default: "chromem"
|
||||
EmbeddingModel string // default: "granite-embedding-107m-multilingual"
|
||||
MaxChunkingSize int // default: 400
|
||||
ChunkOverlap int // default: 0
|
||||
DatabaseURL string
|
||||
AgentHubURL string // default: "https://agenthub.localai.io"
|
||||
}
|
||||
|
||||
type AppOption func(*ApplicationConfig)
|
||||
@@ -104,6 +131,14 @@ func NewApplicationConfig(o ...AppOption) *ApplicationConfig {
|
||||
LRUEvictionRetryInterval: 1 * time.Second, // Default: 1 second
|
||||
WatchDogInterval: 500 * time.Millisecond, // Default: 500ms
|
||||
TracingMaxItems: 1024,
|
||||
AgentPool: AgentPoolConfig{
|
||||
Enabled: true,
|
||||
Timeout: "5m",
|
||||
VectorEngine: "chromem",
|
||||
EmbeddingModel: "granite-embedding-107m-multilingual",
|
||||
MaxChunkingSize: 400,
|
||||
AgentHubURL: "https://agenthub.localai.io",
|
||||
},
|
||||
PathWithoutAuth: []string{
|
||||
"/static/",
|
||||
"/generated-audio/",
|
||||
@@ -541,6 +576,122 @@ func WithHttpGetExemptedEndpoints(endpoints []string) AppOption {
|
||||
}
|
||||
}
|
||||
|
||||
// Agent Pool options
|
||||
|
||||
var DisableAgentPool = func(o *ApplicationConfig) {
|
||||
o.AgentPool.Enabled = false
|
||||
}
|
||||
|
||||
func WithAgentPoolAPIURL(url string) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.APIURL = url
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentPoolAPIKey(key string) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.APIKey = key
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentPoolDefaultModel(model string) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.DefaultModel = model
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentPoolMultimodalModel(model string) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.MultimodalModel = model
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentPoolTranscriptionModel(model string) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.TranscriptionModel = model
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentPoolTranscriptionLanguage(lang string) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.TranscriptionLanguage = lang
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentPoolTTSModel(model string) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.TTSModel = model
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentPoolStateDir(dir string) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.StateDir = dir
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentPoolTimeout(timeout string) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.Timeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
var EnableAgentPoolSkills = func(o *ApplicationConfig) {
|
||||
o.AgentPool.EnableSkills = true
|
||||
}
|
||||
|
||||
func WithAgentPoolVectorEngine(engine string) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.VectorEngine = engine
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentPoolEmbeddingModel(model string) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.EmbeddingModel = model
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentPoolCustomActionsDir(dir string) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.CustomActionsDir = dir
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentPoolDatabaseURL(url string) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.DatabaseURL = url
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentPoolMaxChunkingSize(size int) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.MaxChunkingSize = size
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentPoolChunkOverlap(overlap int) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.ChunkOverlap = overlap
|
||||
}
|
||||
}
|
||||
|
||||
var EnableAgentPoolLogs = func(o *ApplicationConfig) {
|
||||
o.AgentPool.EnableLogs = true
|
||||
}
|
||||
|
||||
func WithAgentPoolCollectionDBPath(path string) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.CollectionDBPath = path
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentHubURL(url string) AppOption {
|
||||
return func(o *ApplicationConfig) {
|
||||
o.AgentPool.AgentHubURL = url
|
||||
}
|
||||
}
|
||||
|
||||
// ToConfigLoaderOptions returns a slice of ConfigLoader Option.
|
||||
// Some options defined at the application level are going to be passed as defaults for
|
||||
// all the configuration for the models.
|
||||
@@ -621,6 +772,15 @@ func (o *ApplicationConfig) ToRuntimeSettings() RuntimeSettings {
|
||||
openResponsesStoreTTL = "0" // default: no expiration
|
||||
}
|
||||
|
||||
// Agent Pool settings
|
||||
agentPoolEnabled := o.AgentPool.Enabled
|
||||
agentPoolDefaultModel := o.AgentPool.DefaultModel
|
||||
agentPoolEmbeddingModel := o.AgentPool.EmbeddingModel
|
||||
agentPoolMaxChunkingSize := o.AgentPool.MaxChunkingSize
|
||||
agentPoolChunkOverlap := o.AgentPool.ChunkOverlap
|
||||
agentPoolEnableLogs := o.AgentPool.EnableLogs
|
||||
agentPoolCollectionDBPath := o.AgentPool.CollectionDBPath
|
||||
|
||||
return RuntimeSettings{
|
||||
WatchdogEnabled: &watchdogEnabled,
|
||||
WatchdogIdleEnabled: &watchdogIdle,
|
||||
@@ -655,6 +815,13 @@ func (o *ApplicationConfig) ToRuntimeSettings() RuntimeSettings {
|
||||
ApiKeys: &apiKeys,
|
||||
AgentJobRetentionDays: &agentJobRetentionDays,
|
||||
OpenResponsesStoreTTL: &openResponsesStoreTTL,
|
||||
AgentPoolEnabled: &agentPoolEnabled,
|
||||
AgentPoolDefaultModel: &agentPoolDefaultModel,
|
||||
AgentPoolEmbeddingModel: &agentPoolEmbeddingModel,
|
||||
AgentPoolMaxChunkingSize: &agentPoolMaxChunkingSize,
|
||||
AgentPoolChunkOverlap: &agentPoolChunkOverlap,
|
||||
AgentPoolEnableLogs: &agentPoolEnableLogs,
|
||||
AgentPoolCollectionDBPath: &agentPoolCollectionDBPath,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -804,6 +971,36 @@ func (o *ApplicationConfig) ApplyRuntimeSettings(settings *RuntimeSettings) (req
|
||||
}
|
||||
// This setting doesn't require restart, can be updated dynamically
|
||||
}
|
||||
// Agent Pool settings
|
||||
if settings.AgentPoolEnabled != nil {
|
||||
o.AgentPool.Enabled = *settings.AgentPoolEnabled
|
||||
requireRestart = true
|
||||
}
|
||||
if settings.AgentPoolDefaultModel != nil {
|
||||
o.AgentPool.DefaultModel = *settings.AgentPoolDefaultModel
|
||||
requireRestart = true
|
||||
}
|
||||
if settings.AgentPoolEmbeddingModel != nil {
|
||||
o.AgentPool.EmbeddingModel = *settings.AgentPoolEmbeddingModel
|
||||
requireRestart = true
|
||||
}
|
||||
if settings.AgentPoolMaxChunkingSize != nil {
|
||||
o.AgentPool.MaxChunkingSize = *settings.AgentPoolMaxChunkingSize
|
||||
requireRestart = true
|
||||
}
|
||||
if settings.AgentPoolChunkOverlap != nil {
|
||||
o.AgentPool.ChunkOverlap = *settings.AgentPoolChunkOverlap
|
||||
requireRestart = true
|
||||
}
|
||||
if settings.AgentPoolEnableLogs != nil {
|
||||
o.AgentPool.EnableLogs = *settings.AgentPoolEnableLogs
|
||||
requireRestart = true
|
||||
}
|
||||
if settings.AgentPoolCollectionDBPath != nil {
|
||||
o.AgentPool.CollectionDBPath = *settings.AgentPoolCollectionDBPath
|
||||
requireRestart = true
|
||||
}
|
||||
|
||||
// Note: ApiKeys requires special handling (merging with startup keys) - handled in caller
|
||||
|
||||
return requireRestart
|
||||
|
||||
@@ -63,4 +63,13 @@ type RuntimeSettings struct {
|
||||
|
||||
// Open Responses settings
|
||||
OpenResponsesStoreTTL *string `json:"open_responses_store_ttl,omitempty"` // TTL for stored responses (e.g., "1h", "30m", "0" = no expiration)
|
||||
|
||||
// Agent Pool settings
|
||||
AgentPoolEnabled *bool `json:"agent_pool_enabled,omitempty"`
|
||||
AgentPoolDefaultModel *string `json:"agent_pool_default_model,omitempty"`
|
||||
AgentPoolEmbeddingModel *string `json:"agent_pool_embedding_model,omitempty"`
|
||||
AgentPoolMaxChunkingSize *int `json:"agent_pool_max_chunking_size,omitempty"`
|
||||
AgentPoolChunkOverlap *int `json:"agent_pool_chunk_overlap,omitempty"`
|
||||
AgentPoolEnableLogs *bool `json:"agent_pool_enable_logs,omitempty"`
|
||||
AgentPoolCollectionDBPath *string `json:"agent_pool_collection_db_path,omitempty"`
|
||||
}
|
||||
|
||||
@@ -240,6 +240,7 @@ func API(application *application.Application) (*echo.Echo, error) {
|
||||
}
|
||||
|
||||
routes.RegisterLocalAIRoutes(e, requestExtractor, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService(), opcache, application.TemplatesEvaluator(), application)
|
||||
routes.RegisterAgentPoolRoutes(e, application)
|
||||
routes.RegisterOpenAIRoutes(e, requestExtractor, application)
|
||||
routes.RegisterAnthropicRoutes(e, requestExtractor, application)
|
||||
routes.RegisterOpenResponsesRoutes(e, requestExtractor, application)
|
||||
|
||||
221
core/http/endpoints/localai/agent_collections.go
Normal file
221
core/http/endpoints/localai/agent_collections.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package localai
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mudler/LocalAI/core/application"
|
||||
)
|
||||
|
||||
func ListCollectionsEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
collections, err := svc.ListCollections()
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]any{
|
||||
"collections": collections,
|
||||
"count": len(collections),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func CreateCollectionEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
var payload struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := c.Bind(&payload); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
if err := svc.CreateCollection(payload.Name); err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusCreated, map[string]string{"status": "ok", "name": payload.Name})
|
||||
}
|
||||
}
|
||||
|
||||
func UploadToCollectionEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
name := c.Param("name")
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "file required"})
|
||||
}
|
||||
if svc.CollectionEntryExists(name, file.Filename) {
|
||||
return c.JSON(http.StatusConflict, map[string]string{"error": "entry already exists"})
|
||||
}
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
defer src.Close()
|
||||
if err := svc.UploadToCollection(name, file.Filename, src); err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "ok", "filename": file.Filename})
|
||||
}
|
||||
}
|
||||
|
||||
func ListCollectionEntriesEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
entries, err := svc.ListCollectionEntries(c.Param("name"))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]any{
|
||||
"entries": entries,
|
||||
"count": len(entries),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func GetCollectionEntryContentEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
entryParam := c.Param("*")
|
||||
entry, err := url.PathUnescape(entryParam)
|
||||
if err != nil {
|
||||
entry = entryParam
|
||||
}
|
||||
content, chunkCount, err := svc.GetCollectionEntryContent(c.Param("name"), entry)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]any{
|
||||
"content": content,
|
||||
"chunk_count": chunkCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func SearchCollectionEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
var payload struct {
|
||||
Query string `json:"query"`
|
||||
MaxResults int `json:"max_results"`
|
||||
}
|
||||
if err := c.Bind(&payload); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
results, err := svc.SearchCollection(c.Param("name"), payload.Query, payload.MaxResults)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]any{
|
||||
"results": results,
|
||||
"count": len(results),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ResetCollectionEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
if err := svc.ResetCollection(c.Param("name")); err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteCollectionEntryEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
var payload struct {
|
||||
Entry string `json:"entry"`
|
||||
}
|
||||
if err := c.Bind(&payload); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
remaining, err := svc.DeleteCollectionEntry(c.Param("name"), payload.Entry)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]any{
|
||||
"remaining_entries": remaining,
|
||||
"count": len(remaining),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func AddCollectionSourceEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
var payload struct {
|
||||
URL string `json:"url"`
|
||||
UpdateInterval int `json:"update_interval"`
|
||||
}
|
||||
if err := c.Bind(&payload); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
if payload.UpdateInterval < 1 {
|
||||
payload.UpdateInterval = 60
|
||||
}
|
||||
if err := svc.AddCollectionSource(c.Param("name"), payload.URL, payload.UpdateInterval); err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
func RemoveCollectionSourceEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
var payload struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
if err := c.Bind(&payload); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
if err := svc.RemoveCollectionSource(c.Param("name"), payload.URL); err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
func ListCollectionSourcesEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
sources, err := svc.ListCollectionSources(c.Param("name"))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]any{
|
||||
"sources": sources,
|
||||
"count": len(sources),
|
||||
})
|
||||
}
|
||||
}
|
||||
213
core/http/endpoints/localai/agent_responses.go
Normal file
213
core/http/endpoints/localai/agent_responses.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package localai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mudler/LocalAI/core/application"
|
||||
coreTypes "github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/xlog"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
// agentResponsesRequest is the minimal subset of the OpenResponses request body
|
||||
// needed to route to an agent.
|
||||
type agentResponsesRequest struct {
|
||||
Model string `json:"model"`
|
||||
Input json.RawMessage `json:"input"`
|
||||
PreviousResponseID string `json:"previous_response_id,omitempty"`
|
||||
Tools []json.RawMessage `json:"tools,omitempty"`
|
||||
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
|
||||
}
|
||||
|
||||
// AgentResponsesInterceptor returns a middleware that intercepts /v1/responses
|
||||
// requests when the model name matches an agent in the pool. If no agent matches,
|
||||
// it restores the request body and falls through to the normal responses pipeline.
|
||||
func AgentResponsesInterceptor(app *application.Application) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
if svc == nil {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
// Read and buffer the body so we can peek at the model name
|
||||
body, err := io.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
return next(c)
|
||||
}
|
||||
// Always restore the body for the next handler
|
||||
c.Request().Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
var req agentResponsesRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil || req.Model == "" {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
// Check if this model name is an agent
|
||||
ag := svc.GetAgent(req.Model)
|
||||
if ag == nil {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
// This is an agent — handle the request directly
|
||||
messages := parseInputToMessages(req.Input)
|
||||
if len(messages) == 0 {
|
||||
return c.JSON(http.StatusBadRequest, map[string]any{
|
||||
"error": map[string]string{
|
||||
"type": "invalid_request_error",
|
||||
"message": "no input messages provided",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
jobOptions := []coreTypes.JobOption{
|
||||
coreTypes.WithConversationHistory(messages),
|
||||
}
|
||||
|
||||
res := ag.Ask(jobOptions...)
|
||||
|
||||
if res == nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]any{
|
||||
"error": map[string]string{
|
||||
"type": "server_error",
|
||||
"message": "agent request failed or was cancelled",
|
||||
},
|
||||
})
|
||||
}
|
||||
if res.Error != nil {
|
||||
xlog.Error("Error asking agent via responses API", "agent", req.Model, "error", res.Error)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]any{
|
||||
"error": map[string]string{
|
||||
"type": "server_error",
|
||||
"message": res.Error.Error(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
id := fmt.Sprintf("resp_%s", uuid.New().String())
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]any{
|
||||
"id": id,
|
||||
"object": "response",
|
||||
"created_at": time.Now().Unix(),
|
||||
"status": "completed",
|
||||
"model": req.Model,
|
||||
"previous_response_id": nil,
|
||||
"output": []any{
|
||||
map[string]any{
|
||||
"type": "message",
|
||||
"id": fmt.Sprintf("msg_%d", time.Now().UnixNano()),
|
||||
"status": "completed",
|
||||
"role": "assistant",
|
||||
"content": []map[string]any{
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": res.Response,
|
||||
"annotations": []any{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseInputToMessages converts the raw JSON input (string or message array) to openai messages.
|
||||
func parseInputToMessages(raw json.RawMessage) []openai.ChatCompletionMessage {
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try as string first
|
||||
var text string
|
||||
if err := json.Unmarshal(raw, &text); err == nil && text != "" {
|
||||
return []openai.ChatCompletionMessage{
|
||||
{Role: "user", Content: text},
|
||||
}
|
||||
}
|
||||
|
||||
// Try as array of message objects
|
||||
var messages []struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
Content json.RawMessage `json:"content,omitempty"`
|
||||
CallId string `json:"call_id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Arguments string `json:"arguments,omitempty"`
|
||||
Output string `json:"output,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &messages); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result []openai.ChatCompletionMessage
|
||||
for _, m := range messages {
|
||||
switch m.Type {
|
||||
case "function_call":
|
||||
result = append(result, openai.ChatCompletionMessage{
|
||||
Role: "assistant",
|
||||
ToolCalls: []openai.ToolCall{
|
||||
{
|
||||
Type: "function",
|
||||
ID: m.CallId,
|
||||
Function: openai.FunctionCall{
|
||||
Arguments: m.Arguments,
|
||||
Name: m.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
case "function_call_output":
|
||||
if m.CallId != "" && m.Output != "" {
|
||||
result = append(result, openai.ChatCompletionMessage{
|
||||
Role: "tool",
|
||||
Content: m.Output,
|
||||
ToolCallID: m.CallId,
|
||||
})
|
||||
}
|
||||
default:
|
||||
if m.Role == "" {
|
||||
continue
|
||||
}
|
||||
content := parseMessageContent(m.Content)
|
||||
if content != "" {
|
||||
result = append(result, openai.ChatCompletionMessage{
|
||||
Role: m.Role,
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// parseMessageContent extracts text from either a string or array of content items.
|
||||
func parseMessageContent(raw json.RawMessage) string {
|
||||
if len(raw) == 0 {
|
||||
return ""
|
||||
}
|
||||
var text string
|
||||
if err := json.Unmarshal(raw, &text); err == nil {
|
||||
return text
|
||||
}
|
||||
var items []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &items); err == nil {
|
||||
for _, item := range items {
|
||||
if item.Type == "text" || item.Type == "input_text" {
|
||||
return item.Text
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
381
core/http/endpoints/localai/agent_skills.go
Normal file
381
core/http/endpoints/localai/agent_skills.go
Normal file
@@ -0,0 +1,381 @@
|
||||
package localai
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mudler/LocalAI/core/application"
|
||||
skilldomain "github.com/mudler/skillserver/pkg/domain"
|
||||
)
|
||||
|
||||
type skillResponse struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Description string `json:"description,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
Compatibility string `json:"compatibility,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
AllowedTools string `json:"allowed-tools,omitempty"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
}
|
||||
|
||||
func skillToResponse(s skilldomain.Skill) skillResponse {
|
||||
out := skillResponse{Name: s.Name, Content: s.Content, ReadOnly: s.ReadOnly}
|
||||
if s.Metadata != nil {
|
||||
out.Description = s.Metadata.Description
|
||||
out.License = s.Metadata.License
|
||||
out.Compatibility = s.Metadata.Compatibility
|
||||
out.Metadata = s.Metadata.Metadata
|
||||
out.AllowedTools = s.Metadata.AllowedTools
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func skillsToResponses(skills []skilldomain.Skill) []skillResponse {
|
||||
out := make([]skillResponse, len(skills))
|
||||
for i, s := range skills {
|
||||
out[i] = skillToResponse(s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ListSkillsEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
skills, err := svc.ListSkills()
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, skillsToResponses(skills))
|
||||
}
|
||||
}
|
||||
|
||||
func GetSkillsConfigEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
cfg := svc.GetSkillsConfig()
|
||||
return c.JSON(http.StatusOK, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func SearchSkillsEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
query := c.QueryParam("q")
|
||||
skills, err := svc.SearchSkills(query)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, skillsToResponses(skills))
|
||||
}
|
||||
}
|
||||
|
||||
func CreateSkillEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
var payload struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Content string `json:"content"`
|
||||
License string `json:"license,omitempty"`
|
||||
Compatibility string `json:"compatibility,omitempty"`
|
||||
AllowedTools string `json:"allowed-tools,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
if err := c.Bind(&payload); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
skill, err := svc.CreateSkill(payload.Name, payload.Description, payload.Content, payload.License, payload.Compatibility, payload.AllowedTools, payload.Metadata)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "already exists") {
|
||||
return c.JSON(http.StatusConflict, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusCreated, skillToResponse(*skill))
|
||||
}
|
||||
}
|
||||
|
||||
func GetSkillEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
skill, err := svc.GetSkill(c.Param("name"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, skillToResponse(*skill))
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateSkillEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
var payload struct {
|
||||
Description string `json:"description"`
|
||||
Content string `json:"content"`
|
||||
License string `json:"license,omitempty"`
|
||||
Compatibility string `json:"compatibility,omitempty"`
|
||||
AllowedTools string `json:"allowed-tools,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
if err := c.Bind(&payload); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
skill, err := svc.UpdateSkill(c.Param("name"), payload.Description, payload.Content, payload.License, payload.Compatibility, payload.AllowedTools, payload.Metadata)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, skillToResponse(*skill))
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteSkillEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
if err := svc.DeleteSkill(c.Param("name")); err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
func ExportSkillEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
// The wildcard param captures the path after /export/
|
||||
name := c.Param("*")
|
||||
data, err := svc.ExportSkill(name)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
c.Response().Header().Set("Content-Disposition", "attachment; filename="+name+".tar.gz")
|
||||
c.Response().Header().Set("Content-Type", "application/gzip")
|
||||
return c.Blob(http.StatusOK, "application/gzip", data)
|
||||
}
|
||||
}
|
||||
|
||||
func ImportSkillEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "file required"})
|
||||
}
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
defer src.Close()
|
||||
data, err := io.ReadAll(src)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
skill, err := svc.ImportSkill(data)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusCreated, skill)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Skill Resources ---
|
||||
|
||||
func ListSkillResourcesEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
resources, skill, err := svc.ListSkillResources(c.Param("name"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
scripts := []map[string]any{}
|
||||
references := []map[string]any{}
|
||||
assets := []map[string]any{}
|
||||
for _, res := range resources {
|
||||
m := map[string]any{
|
||||
"path": res.Path,
|
||||
"name": res.Name,
|
||||
"size": res.Size,
|
||||
"mime_type": res.MimeType,
|
||||
"readable": res.Readable,
|
||||
"modified": res.Modified.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
switch res.Type {
|
||||
case "script":
|
||||
scripts = append(scripts, m)
|
||||
case "reference":
|
||||
references = append(references, m)
|
||||
case "asset":
|
||||
assets = append(assets, m)
|
||||
}
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]any{
|
||||
"scripts": scripts,
|
||||
"references": references,
|
||||
"assets": assets,
|
||||
"readOnly": skill.ReadOnly,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func GetSkillResourceEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
content, info, err := svc.GetSkillResource(c.Param("name"), c.Param("*"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
if c.QueryParam("encoding") == "base64" || !info.Readable {
|
||||
return c.JSON(http.StatusOK, map[string]any{
|
||||
"content": content.Content,
|
||||
"encoding": content.Encoding,
|
||||
"mime_type": content.MimeType,
|
||||
"size": content.Size,
|
||||
})
|
||||
}
|
||||
c.Response().Header().Set("Content-Type", content.MimeType)
|
||||
return c.String(http.StatusOK, content.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateSkillResourceEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "file is required"})
|
||||
}
|
||||
path := c.FormValue("path")
|
||||
if path == "" {
|
||||
path = file.Filename
|
||||
}
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "failed to open file"})
|
||||
}
|
||||
defer src.Close()
|
||||
data, err := io.ReadAll(src)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
if err := svc.CreateSkillResource(c.Param("name"), path, data); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusCreated, map[string]string{"path": path})
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateSkillResourceEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
var payload struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := c.Bind(&payload); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
if err := svc.UpdateSkillResource(c.Param("name"), c.Param("*"), payload.Content); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteSkillResourceEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
if err := svc.DeleteSkillResource(c.Param("name"), c.Param("*")); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Git Repos ---
|
||||
|
||||
func ListGitReposEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
repos, err := svc.ListGitRepos()
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, repos)
|
||||
}
|
||||
}
|
||||
|
||||
func AddGitRepoEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
var payload struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
if err := c.Bind(&payload); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
repo, err := svc.AddGitRepo(payload.URL)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusCreated, repo)
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateGitRepoEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
var payload struct {
|
||||
URL string `json:"url"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
if err := c.Bind(&payload); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
repo, err := svc.UpdateGitRepo(c.Param("id"), payload.URL, payload.Enabled)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, repo)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteGitRepoEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
if err := svc.DeleteGitRepo(c.Param("id")); err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
func SyncGitRepoEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
if err := svc.SyncGitRepo(c.Param("id")); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusAccepted, map[string]string{"status": "syncing"})
|
||||
}
|
||||
}
|
||||
|
||||
func ToggleGitRepoEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
repo, err := svc.ToggleGitRepo(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, repo)
|
||||
}
|
||||
}
|
||||
333
core/http/endpoints/localai/agents.go
Normal file
333
core/http/endpoints/localai/agents.go
Normal file
@@ -0,0 +1,333 @@
|
||||
package localai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mudler/LocalAI/core/application"
|
||||
"github.com/mudler/LocalAI/core/services"
|
||||
"github.com/mudler/LocalAGI/core/state"
|
||||
coreTypes "github.com/mudler/LocalAGI/core/types"
|
||||
agiServices "github.com/mudler/LocalAGI/services"
|
||||
)
|
||||
|
||||
func ListAgentsEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
statuses := svc.ListAgents()
|
||||
agents := make([]string, 0, len(statuses))
|
||||
for name := range statuses {
|
||||
agents = append(agents, name)
|
||||
}
|
||||
sort.Strings(agents)
|
||||
resp := map[string]any{
|
||||
"agents": agents,
|
||||
"agentCount": len(agents),
|
||||
"actions": len(agiServices.AvailableActions),
|
||||
"connectors": len(agiServices.AvailableConnectors),
|
||||
"statuses": statuses,
|
||||
}
|
||||
if hubURL := svc.AgentHubURL(); hubURL != "" {
|
||||
resp["agent_hub_url"] = hubURL
|
||||
}
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateAgentEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
var cfg state.AgentConfig
|
||||
if err := c.Bind(&cfg); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
if err := svc.CreateAgent(&cfg); err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusCreated, map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
func GetAgentEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
name := c.Param("name")
|
||||
ag := svc.GetAgent(name)
|
||||
if ag == nil {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "Agent not found"})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]any{
|
||||
"active": !ag.Paused(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateAgentEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
name := c.Param("name")
|
||||
var cfg state.AgentConfig
|
||||
if err := c.Bind(&cfg); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
if err := svc.UpdateAgent(name, &cfg); err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteAgentEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
name := c.Param("name")
|
||||
if err := svc.DeleteAgent(name); err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
func GetAgentConfigEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
name := c.Param("name")
|
||||
cfg := svc.GetAgentConfig(name)
|
||||
if cfg == nil {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "Agent not found"})
|
||||
}
|
||||
return c.JSON(http.StatusOK, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func PauseAgentEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
if err := svc.PauseAgent(c.Param("name")); err != nil {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
func ResumeAgentEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
if err := svc.ResumeAgent(c.Param("name")); err != nil {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
func GetAgentStatusEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
name := c.Param("name")
|
||||
history := svc.GetAgentStatus(name)
|
||||
if history == nil {
|
||||
history = &state.Status{ActionResults: []coreTypes.ActionState{}}
|
||||
}
|
||||
entries := []string{}
|
||||
for i := len(history.Results()) - 1; i >= 0; i-- {
|
||||
h := history.Results()[i]
|
||||
actionName := ""
|
||||
if h.ActionCurrentState.Action != nil {
|
||||
actionName = h.ActionCurrentState.Action.Definition().Name.String()
|
||||
}
|
||||
entries = append(entries, fmt.Sprintf("Reasoning: %s\nAction taken: %s\nParameters: %+v\nResult: %s",
|
||||
h.Reasoning,
|
||||
actionName,
|
||||
h.ActionCurrentState.Params,
|
||||
h.Result))
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]any{
|
||||
"Name": name,
|
||||
"History": entries,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func GetAgentObservablesEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
name := c.Param("name")
|
||||
history, err := svc.GetAgentObservables(name)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]any{
|
||||
"Name": name,
|
||||
"History": history,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ClearAgentObservablesEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
name := c.Param("name")
|
||||
if err := svc.ClearAgentObservables(name); err != nil {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]any{"Name": name, "cleared": true})
|
||||
}
|
||||
}
|
||||
|
||||
func ChatWithAgentEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
name := c.Param("name")
|
||||
var payload struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := c.Bind(&payload); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request format"})
|
||||
}
|
||||
message := strings.TrimSpace(payload.Message)
|
||||
if message == "" {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Message cannot be empty"})
|
||||
}
|
||||
messageID, err := svc.Chat(name, message)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusAccepted, map[string]any{
|
||||
"status": "message_received",
|
||||
"message_id": messageID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func AgentSSEEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
name := c.Param("name")
|
||||
manager := svc.GetSSEManager(name)
|
||||
if manager == nil {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "Agent not found"})
|
||||
}
|
||||
return services.HandleSSE(c, manager)
|
||||
}
|
||||
}
|
||||
|
||||
func GetAgentConfigMetaEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
return c.JSON(http.StatusOK, svc.GetConfigMeta())
|
||||
}
|
||||
}
|
||||
|
||||
func ExportAgentEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
name := c.Param("name")
|
||||
data, err := svc.ExportAgent(name)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.json", name))
|
||||
return c.JSONBlob(http.StatusOK, data)
|
||||
}
|
||||
}
|
||||
|
||||
func ImportAgentEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
|
||||
// Try multipart form file first
|
||||
file, err := c.FormFile("file")
|
||||
if err == nil {
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "failed to open file"})
|
||||
}
|
||||
defer src.Close()
|
||||
data, err := io.ReadAll(src)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "failed to read file"})
|
||||
}
|
||||
if err := svc.ImportAgent(data); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusCreated, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
// Try JSON body
|
||||
var cfg state.AgentConfig
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(&cfg); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request: provide a file or JSON body"})
|
||||
}
|
||||
data, err := json.Marshal(&cfg)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
if err := svc.ImportAgent(data); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusCreated, map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
func ListActionsEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
return c.JSON(http.StatusOK, map[string]any{
|
||||
"actions": svc.ListAvailableActions(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func GetActionDefinitionEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
actionName := c.Param("name")
|
||||
|
||||
var payload struct {
|
||||
Config map[string]string `json:"config"`
|
||||
}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(&payload); err != nil {
|
||||
payload.Config = map[string]string{}
|
||||
}
|
||||
|
||||
def, err := svc.GetActionDefinition(actionName, payload.Config)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, def)
|
||||
}
|
||||
}
|
||||
|
||||
func ExecuteActionEndpoint(app *application.Application) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
svc := app.AgentPoolService()
|
||||
actionName := c.Param("name")
|
||||
|
||||
var payload struct {
|
||||
Config map[string]string `json:"config"`
|
||||
Params coreTypes.ActionParams `json:"params"`
|
||||
}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(&payload); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
}
|
||||
|
||||
result, err := svc.ExecuteAction(c.Request().Context(), actionName, payload.Config, payload.Params)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/mudler/LocalAI/core/templates"
|
||||
"github.com/mudler/LocalAI/pkg/model"
|
||||
"github.com/mudler/cogito"
|
||||
"github.com/mudler/cogito/clients"
|
||||
"github.com/mudler/xlog"
|
||||
)
|
||||
|
||||
@@ -121,7 +122,7 @@ func MCPEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
||||
// and act like completion.go.
|
||||
// We can do this as cogito expects an interface and we can create one that
|
||||
// we satisfy to just call internally ComputeChoices
|
||||
defaultLLM := cogito.NewOpenAILLM(config.Name, apiKey, "http://127.0.0.1:"+port)
|
||||
defaultLLM := clients.NewLocalAILLM(config.Name, apiKey, "http://127.0.0.1:"+port)
|
||||
|
||||
// Build cogito options using the consolidated method
|
||||
cogitoOpts := config.BuildCogitoOptions()
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
reason "github.com/mudler/LocalAI/pkg/reasoning"
|
||||
"github.com/mudler/LocalAI/pkg/utils"
|
||||
"github.com/mudler/cogito"
|
||||
"github.com/mudler/cogito/clients"
|
||||
"github.com/mudler/xlog"
|
||||
)
|
||||
|
||||
@@ -1128,7 +1129,7 @@ func handleBackgroundMCPResponse(ctx context.Context, store *ResponseStore, resp
|
||||
}
|
||||
|
||||
// Create OpenAI LLM client
|
||||
defaultLLM := cogito.NewOpenAILLM(cfg.Name, apiKey, "http://127.0.0.1:"+port)
|
||||
defaultLLM := clients.NewLocalAILLM(cfg.Name, apiKey, "http://127.0.0.1:"+port)
|
||||
|
||||
// Build cogito options
|
||||
cogitoOpts := cfg.BuildCogitoOptions()
|
||||
@@ -2696,7 +2697,7 @@ func handleMCPResponse(c echo.Context, responseID string, createdAt int64, input
|
||||
defer cancel()
|
||||
|
||||
// Create OpenAI LLM client
|
||||
defaultLLM := cogito.NewOpenAILLM(cfg.Name, apiKey, "http://127.0.0.1:"+port)
|
||||
defaultLLM := clients.NewLocalAILLM(cfg.Name, apiKey, "http://127.0.0.1:"+port)
|
||||
|
||||
// Build cogito options
|
||||
cogitoOpts := cfg.BuildCogitoOptions()
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
|
||||
.sidebar-logo-img {
|
||||
width: 100%;
|
||||
max-width: 140px;
|
||||
height: auto;
|
||||
padding: 0 var(--spacing-xs);
|
||||
}
|
||||
|
||||
155
core/http/react-ui/src/components/SearchableModelSelect.jsx
Normal file
155
core/http/react-ui/src/components/SearchableModelSelect.jsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useModels } from '../hooks/useModels'
|
||||
|
||||
export default function SearchableModelSelect({ value, onChange, capability, placeholder = 'Type or select a model...', style }) {
|
||||
const { models, loading } = useModels(capability)
|
||||
const [query, setQuery] = useState('')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [focusIndex, setFocusIndex] = useState(-1)
|
||||
const wrapperRef = useRef(null)
|
||||
const listRef = useRef(null)
|
||||
|
||||
// Sync external value into the input
|
||||
useEffect(() => {
|
||||
setQuery(value || '')
|
||||
}, [value])
|
||||
|
||||
// Close on outside click
|
||||
useEffect(() => {
|
||||
const handler = (e) => {
|
||||
if (wrapperRef.current && !wrapperRef.current.contains(e.target)) setOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', handler)
|
||||
return () => document.removeEventListener('mousedown', handler)
|
||||
}, [])
|
||||
|
||||
const filtered = models.filter(m =>
|
||||
m.id.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
|
||||
const commit = useCallback((val) => {
|
||||
setQuery(val)
|
||||
onChange(val)
|
||||
setOpen(false)
|
||||
setFocusIndex(-1)
|
||||
}, [onChange])
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (!open && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
|
||||
setOpen(true)
|
||||
return
|
||||
}
|
||||
if (!open) return
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
setFocusIndex(i => Math.min(i + 1, filtered.length - 1))
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
setFocusIndex(i => Math.max(i - 1, 0))
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
if (focusIndex >= 0 && focusIndex < filtered.length) {
|
||||
commit(filtered[focusIndex].id)
|
||||
} else {
|
||||
commit(query)
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
setOpen(false)
|
||||
setFocusIndex(-1)
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll focused item into view
|
||||
useEffect(() => {
|
||||
if (focusIndex >= 0 && listRef.current) {
|
||||
const item = listRef.current.children[focusIndex]
|
||||
if (item) item.scrollIntoView({ block: 'nearest' })
|
||||
}
|
||||
}, [focusIndex])
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} className="searchable-model-select" style={style}>
|
||||
<style>{`
|
||||
.searchable-model-select {
|
||||
position: relative;
|
||||
width: 280px;
|
||||
}
|
||||
.searchable-model-select input {
|
||||
width: 100%;
|
||||
}
|
||||
.sms-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
background: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.sms-item {
|
||||
padding: 6px 10px;
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.sms-item:hover, .sms-item.sms-focused {
|
||||
background: var(--color-bg-tertiary);
|
||||
}
|
||||
.sms-item.sms-active {
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.sms-empty {
|
||||
padding: 8px 10px;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
`}</style>
|
||||
<input
|
||||
className="input"
|
||||
value={query}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value)
|
||||
setOpen(true)
|
||||
setFocusIndex(-1)
|
||||
// Commit on every keystroke so the parent always has current value
|
||||
onChange(e.target.value)
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={loading ? 'Loading models...' : placeholder}
|
||||
/>
|
||||
{open && !loading && (
|
||||
<div className="sms-dropdown" ref={listRef}>
|
||||
{filtered.length === 0 ? (
|
||||
<div className="sms-empty">
|
||||
{query ? 'No matching models — value will be used as-is' : 'No models available'}
|
||||
</div>
|
||||
) : (
|
||||
filtered.map((m, i) => (
|
||||
<div
|
||||
key={m.id}
|
||||
className={`sms-item${i === focusIndex ? ' sms-focused' : ''}${m.id === value ? ' sms-active' : ''}`}
|
||||
onMouseEnter={() => setFocusIndex(i)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
commit(m.id)
|
||||
}}
|
||||
>
|
||||
{m.id}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import ThemeToggle from './ThemeToggle'
|
||||
|
||||
@@ -12,13 +13,16 @@ const mainItems = [
|
||||
{ path: '/talk', icon: 'fas fa-phone', label: 'Talk' },
|
||||
]
|
||||
|
||||
const toolItems = [
|
||||
{ path: '/agent-jobs', icon: 'fas fa-tasks', label: 'Agent Jobs' },
|
||||
{ path: '/traces', icon: 'fas fa-chart-line', label: 'Traces' },
|
||||
const agentItems = [
|
||||
{ path: '/agents', icon: 'fas fa-robot', label: 'Agents' },
|
||||
{ path: '/skills', icon: 'fas fa-wand-magic-sparkles', label: 'Skills' },
|
||||
{ path: '/collections', icon: 'fas fa-database', label: 'Memory' },
|
||||
{ path: '/agent-jobs', icon: 'fas fa-tasks', label: 'MCP CI Jobs', feature: 'mcp' },
|
||||
]
|
||||
|
||||
const systemItems = [
|
||||
{ path: '/backends', icon: 'fas fa-server', label: 'Backends' },
|
||||
{ path: '/traces', icon: 'fas fa-chart-line', label: 'Traces' },
|
||||
{ path: '/p2p', icon: 'fas fa-circle-nodes', label: 'Swarm' },
|
||||
{ path: '/manage', icon: 'fas fa-desktop', label: 'System' },
|
||||
{ path: '/settings', icon: 'fas fa-cog', label: 'Settings' },
|
||||
@@ -41,6 +45,11 @@ function NavItem({ item, onClose }) {
|
||||
}
|
||||
|
||||
export default function Sidebar({ isOpen, onClose }) {
|
||||
const [features, setFeatures] = useState({})
|
||||
useEffect(() => {
|
||||
fetch('/api/features').then(r => r.json()).then(setFeatures).catch(() => {})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && <div className="sidebar-overlay" onClick={onClose} />}
|
||||
@@ -65,13 +74,15 @@ export default function Sidebar({ isOpen, onClose }) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tools section */}
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-section-title">Tools</div>
|
||||
{toolItems.map(item => (
|
||||
<NavItem key={item.path} item={item} onClose={onClose} />
|
||||
))}
|
||||
</div>
|
||||
{/* Agents section */}
|
||||
{features.agents !== false && (
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-section-title">Agents</div>
|
||||
{agentItems.filter(item => !item.feature || features[item.feature] !== false).map(item => (
|
||||
<NavItem key={item.path} item={item} onClose={onClose} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* System section */}
|
||||
<div className="sidebar-section">
|
||||
|
||||
279
core/http/react-ui/src/pages/AgentChat.jsx
Normal file
279
core/http/react-ui/src/pages/AgentChat.jsx
Normal file
@@ -0,0 +1,279 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useParams, useNavigate, useOutletContext } from 'react-router-dom'
|
||||
import { agentsApi } from '../utils/api'
|
||||
|
||||
export default function AgentChat() {
|
||||
const { name } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { addToast } = useOutletContext()
|
||||
const [messages, setMessages] = useState([])
|
||||
const [input, setInput] = useState('')
|
||||
const [processing, setProcessing] = useState(false)
|
||||
const messagesEndRef = useRef(null)
|
||||
const textareaRef = useRef(null)
|
||||
const eventSourceRef = useRef(null)
|
||||
const messageIdCounter = useRef(0)
|
||||
|
||||
const nextId = useCallback(() => {
|
||||
messageIdCounter.current += 1
|
||||
return messageIdCounter.current
|
||||
}, [])
|
||||
|
||||
// Connect to SSE endpoint
|
||||
useEffect(() => {
|
||||
const url = `/api/agents/${encodeURIComponent(name)}/sse`
|
||||
const es = new EventSource(url)
|
||||
eventSourceRef.current = es
|
||||
|
||||
es.addEventListener('json_message', (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data)
|
||||
setMessages(prev => [...prev, {
|
||||
id: nextId(),
|
||||
sender: data.sender || (data.role === 'user' ? 'user' : 'agent'),
|
||||
content: data.content || data.message || '',
|
||||
timestamp: data.timestamp || Date.now(),
|
||||
}])
|
||||
} catch (_err) {
|
||||
// ignore malformed messages
|
||||
}
|
||||
})
|
||||
|
||||
es.addEventListener('json_message_status', (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data)
|
||||
if (data.status === 'processing') {
|
||||
setProcessing(true)
|
||||
} else if (data.status === 'completed') {
|
||||
setProcessing(false)
|
||||
}
|
||||
} catch (_err) {
|
||||
// ignore
|
||||
}
|
||||
})
|
||||
|
||||
es.addEventListener('status', (e) => {
|
||||
const text = e.data
|
||||
if (!text) return
|
||||
setMessages(prev => [...prev, {
|
||||
id: nextId(),
|
||||
sender: 'system',
|
||||
content: text,
|
||||
timestamp: Date.now(),
|
||||
}])
|
||||
})
|
||||
|
||||
es.addEventListener('json_error', (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data)
|
||||
addToast(data.error || data.message || 'Agent error', 'error')
|
||||
} catch (_err) {
|
||||
addToast('Agent error', 'error')
|
||||
}
|
||||
setProcessing(false)
|
||||
})
|
||||
|
||||
es.onerror = () => {
|
||||
addToast('SSE connection lost, attempting to reconnect...', 'warning')
|
||||
}
|
||||
|
||||
return () => {
|
||||
es.close()
|
||||
eventSourceRef.current = null
|
||||
}
|
||||
}, [name, addToast, nextId])
|
||||
|
||||
// Auto-scroll to bottom
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [messages])
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
const msg = input.trim()
|
||||
if (!msg || processing) return
|
||||
setInput('')
|
||||
if (textareaRef.current) textareaRef.current.style.height = 'auto'
|
||||
setProcessing(true)
|
||||
try {
|
||||
await agentsApi.chat(name, msg)
|
||||
} catch (err) {
|
||||
addToast(`Failed to send message: ${err.message}`, 'error')
|
||||
setProcessing(false)
|
||||
}
|
||||
}, [input, processing, name, addToast])
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page agent-chat-page">
|
||||
<style>{`
|
||||
.agent-chat-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 80px);
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
.agent-chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
.agent-chat-message {
|
||||
display: flex;
|
||||
max-width: 75%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.agent-chat-message-user {
|
||||
align-self: flex-end;
|
||||
}
|
||||
.agent-chat-message-agent {
|
||||
align-self: flex-start;
|
||||
}
|
||||
.agent-chat-bubble {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.agent-chat-message-user .agent-chat-bubble {
|
||||
background: var(--color-bg-tertiary, #e5e7eb);
|
||||
color: var(--color-text-primary);
|
||||
border-bottom-right-radius: var(--radius-xs, 4px);
|
||||
}
|
||||
.agent-chat-message-agent .agent-chat-bubble {
|
||||
background: var(--color-primary, #3b82f6);
|
||||
color: #fff;
|
||||
border-bottom-left-radius: var(--radius-xs, 4px);
|
||||
}
|
||||
.agent-chat-message-system {
|
||||
align-self: center;
|
||||
max-width: 90%;
|
||||
}
|
||||
.agent-chat-message-system .agent-chat-bubble {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
}
|
||||
.agent-chat-timestamp {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: 2px;
|
||||
padding: 0 var(--spacing-xs);
|
||||
}
|
||||
.agent-chat-message-user .agent-chat-timestamp {
|
||||
text-align: right;
|
||||
}
|
||||
.agent-chat-input-area {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-md);
|
||||
border-top: 1px solid var(--color-border);
|
||||
background: var(--color-bg-secondary);
|
||||
align-items: flex-end;
|
||||
}
|
||||
.agent-chat-input-area textarea {
|
||||
flex: 1;
|
||||
min-height: 38px;
|
||||
max-height: 150px;
|
||||
resize: none;
|
||||
overflow-y: auto;
|
||||
line-height: 1.5;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
.agent-chat-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h1 className="page-title">
|
||||
<i className="fas fa-robot" style={{ marginRight: 'var(--spacing-xs)' }} />
|
||||
{name}
|
||||
</h1>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)' }}>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => navigate(`/agents/${encodeURIComponent(name)}/status`)} title="View status & observables">
|
||||
<i className="fas fa-chart-bar" /> Status
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => setMessages([])} disabled={messages.length === 0} title="Clear chat history">
|
||||
<i className="fas fa-eraser" /> Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="agent-chat-messages">
|
||||
{messages.length === 0 && !processing && (
|
||||
<div className="agent-chat-empty">
|
||||
Send a message to start chatting with {name}.
|
||||
</div>
|
||||
)}
|
||||
{messages.map(msg => (
|
||||
<div key={msg.id} className={`agent-chat-message agent-chat-message-${msg.sender}`}>
|
||||
<div>
|
||||
{msg.sender === 'system'
|
||||
? <div className="agent-chat-bubble" dangerouslySetInnerHTML={{ __html: msg.content }} />
|
||||
: <div className="agent-chat-bubble">{msg.content}</div>
|
||||
}
|
||||
<div className="agent-chat-timestamp">
|
||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{processing && (
|
||||
<div className="agent-chat-message agent-chat-message-agent">
|
||||
<div>
|
||||
<div className="agent-chat-bubble">
|
||||
<i className="fas fa-circle-notch fa-spin" /> Thinking...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
<div className="agent-chat-input-area">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="input"
|
||||
value={input}
|
||||
onChange={(e) => {
|
||||
setInput(e.target.value)
|
||||
// Auto-resize
|
||||
const ta = e.target
|
||||
ta.style.height = 'auto'
|
||||
ta.style.height = Math.min(ta.scrollHeight, 150) + 'px'
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type a message... (Shift+Enter for new line)"
|
||||
disabled={processing}
|
||||
rows={1}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleSend}
|
||||
disabled={processing || !input.trim()}
|
||||
>
|
||||
<i className="fas fa-paper-plane" /> Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
849
core/http/react-ui/src/pages/AgentCreate.jsx
Normal file
849
core/http/react-ui/src/pages/AgentCreate.jsx
Normal file
@@ -0,0 +1,849 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { useParams, useNavigate, useLocation, useOutletContext } from 'react-router-dom'
|
||||
import { agentsApi } from '../utils/api'
|
||||
import SearchableModelSelect from '../components/SearchableModelSelect'
|
||||
|
||||
// --- MCP STDIO helpers ---
|
||||
|
||||
function parseStdioServers(value) {
|
||||
if (!value) return []
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(s => ({
|
||||
name: s.name || '',
|
||||
command: s.cmd || s.command || '',
|
||||
args: Array.isArray(s.args) ? [...s.args] : [],
|
||||
env: Array.isArray(s.env) ? [...s.env]
|
||||
: (s.env && typeof s.env === 'object') ? Object.entries(s.env).map(([k, v]) => `${k}=${v}`) : [],
|
||||
}))
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
if (parsed.mcpServers) {
|
||||
return Object.entries(parsed.mcpServers).map(([name, srv]) => ({
|
||||
name,
|
||||
command: srv.command || '',
|
||||
args: srv.args || [],
|
||||
env: Object.entries(srv.env || {}).map(([k, v]) => `${k}=${v}`),
|
||||
}))
|
||||
}
|
||||
if (Array.isArray(parsed)) return parseStdioServers(parsed)
|
||||
} catch { /* not valid JSON */ }
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function buildStdioJson(list) {
|
||||
const mcpServers = {}
|
||||
const usedKeys = new Set()
|
||||
list.forEach((item, index) => {
|
||||
let key = item.name?.trim() || `server${index}`
|
||||
while (usedKeys.has(key)) key = `${key}_${index}`
|
||||
usedKeys.add(key)
|
||||
const envMap = {}
|
||||
for (const e of (item.env || [])) {
|
||||
const eqIdx = e.indexOf('=')
|
||||
if (eqIdx > 0) envMap[e.slice(0, eqIdx)] = e.slice(eqIdx + 1)
|
||||
}
|
||||
mcpServers[key] = { command: item.command || '', args: item.args || [], env: envMap }
|
||||
})
|
||||
return JSON.stringify({ mcpServers }, null, 2)
|
||||
}
|
||||
|
||||
// --- Shared UI components (same style as Settings page) ---
|
||||
|
||||
function Toggle({ checked, onChange, disabled }) {
|
||||
return (
|
||||
<label style={{
|
||||
position: 'relative', display: 'inline-block', width: 40, height: 22, cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked || false}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
disabled={disabled}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<span style={{
|
||||
position: 'absolute', inset: 0, borderRadius: 22,
|
||||
background: checked ? 'var(--color-primary)' : 'var(--color-toggle-off)',
|
||||
transition: 'background 200ms',
|
||||
}}>
|
||||
<span style={{
|
||||
position: 'absolute', top: 2, left: checked ? 20 : 2,
|
||||
width: 18, height: 18, borderRadius: '50%',
|
||||
background: '#fff', transition: 'left 200ms',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.2)',
|
||||
}} />
|
||||
</span>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingRow({ label, description, children }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: 'var(--spacing-sm) 0',
|
||||
borderBottom: '1px solid var(--color-border-subtle)',
|
||||
}}>
|
||||
<div style={{ flex: 1, marginRight: 'var(--spacing-md)' }}>
|
||||
<div style={{ fontSize: '0.875rem', fontWeight: 500 }}>{label}</div>
|
||||
{description && <div style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)', marginTop: 2 }}>{description}</div>}
|
||||
</div>
|
||||
<div style={{ flexShrink: 0 }}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Form field components ---
|
||||
|
||||
function FormField({ field, value, onChange, disabled }) {
|
||||
const id = `field-${field.name}`
|
||||
const label = field.required
|
||||
? <>{field.label} <span style={{ color: 'var(--color-error)' }}>*</span></>
|
||||
: field.label
|
||||
|
||||
switch (field.type) {
|
||||
case 'checkbox':
|
||||
return (
|
||||
<SettingRow label={label} description={field.helpText}>
|
||||
<Toggle
|
||||
checked={value === true || value === 'true'}
|
||||
onChange={(v) => onChange(field.name, v)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</SettingRow>
|
||||
)
|
||||
case 'select':
|
||||
return (
|
||||
<SettingRow label={label} description={field.helpText}>
|
||||
<select id={id} className="input" style={{ width: 200 }} value={value ?? ''} onChange={(e) => onChange(field.name, e.target.value)} disabled={disabled}>
|
||||
<option value="">— Select —</option>
|
||||
{(field.options || []).map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</SettingRow>
|
||||
)
|
||||
case 'textarea':
|
||||
return (
|
||||
<div style={{ padding: 'var(--spacing-sm) 0', borderBottom: '1px solid var(--color-border-subtle)' }}>
|
||||
<div style={{ fontSize: '0.875rem', fontWeight: 500, marginBottom: 4 }}>{label}</div>
|
||||
{field.helpText && <div style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)', marginBottom: 'var(--spacing-xs)' }}>{field.helpText}</div>}
|
||||
<textarea
|
||||
id={id}
|
||||
className="textarea"
|
||||
value={value ?? ''}
|
||||
onChange={(e) => onChange(field.name, e.target.value)}
|
||||
placeholder={field.placeholder || ''}
|
||||
rows={5}
|
||||
disabled={disabled}
|
||||
style={field.name.includes('prompt') || field.name.includes('template') || field.name.includes('script')
|
||||
? { fontFamily: "'JetBrains Mono', monospace", fontSize: '0.8125rem' } : undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
case 'number':
|
||||
return (
|
||||
<SettingRow label={label} description={field.helpText}>
|
||||
<input
|
||||
id={id} className="input" type="number" style={{ width: 120 }}
|
||||
value={value ?? ''} onChange={(e) => onChange(field.name, e.target.value)}
|
||||
placeholder={field.placeholder || ''} min={field.min} max={field.max} step={field.step}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</SettingRow>
|
||||
)
|
||||
default: {
|
||||
const isModelField = /^(model|multimodal_model|transcription_model|tts_model|embedding_model)$/.test(field.name)
|
||||
if (isModelField && !disabled && !field.disabled) {
|
||||
const capabilityMap = {
|
||||
model: 'FLAG_CHAT',
|
||||
multimodal_model: 'FLAG_CHAT',
|
||||
transcription_model: 'FLAG_TRANSCRIPT',
|
||||
tts_model: 'FLAG_TTS',
|
||||
embedding_model: undefined,
|
||||
}
|
||||
return (
|
||||
<SettingRow label={label} description={field.helpText}>
|
||||
<SearchableModelSelect
|
||||
value={value ?? ''}
|
||||
onChange={(v) => onChange(field.name, v)}
|
||||
capability={capabilityMap[field.name]}
|
||||
placeholder={field.placeholder || 'Type or select a model...'}
|
||||
style={{ width: 250 }}
|
||||
/>
|
||||
</SettingRow>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<SettingRow label={label} description={field.helpText}>
|
||||
<input
|
||||
id={id} className="input" type={field.type === 'password' ? 'password' : 'text'}
|
||||
style={{ width: field.type === 'password' ? 200 : 250 }}
|
||||
value={value ?? ''} onChange={(e) => onChange(field.name, e.target.value)}
|
||||
placeholder={field.placeholder || ''} required={field.required}
|
||||
disabled={disabled || field.disabled}
|
||||
/>
|
||||
</SettingRow>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- ConfigForm for connectors/actions/filters/dynamic_prompts ---
|
||||
|
||||
function ConfigForm({ items, fieldGroups, onChange, onRemove, onAdd, itemType, typeField, addButtonText }) {
|
||||
const typeOptions = [
|
||||
{ value: '', label: `Select a ${itemType} type` },
|
||||
...(fieldGroups || []).map(g => ({ value: g.name, label: g.label })),
|
||||
]
|
||||
|
||||
const parseConfig = (item) => {
|
||||
if (!item?.config) return {}
|
||||
try { return typeof item.config === 'string' ? JSON.parse(item.config || '{}') : item.config }
|
||||
catch { return {} }
|
||||
}
|
||||
|
||||
const handleConfigFieldChange = (index, fieldName, fieldValue, fieldType) => {
|
||||
const config = parseConfig(items[index])
|
||||
config[fieldName] = fieldType === 'checkbox' ? (fieldValue ? 'true' : 'false') : String(fieldValue)
|
||||
onChange(index, { ...items[index], config: JSON.stringify(config) })
|
||||
}
|
||||
|
||||
const label = itemType.charAt(0).toUpperCase() + itemType.slice(1).replace('_', ' ')
|
||||
|
||||
if (!fieldGroups?.length) {
|
||||
return <p style={{ color: 'var(--color-text-muted)', fontSize: '0.875rem' }}>No {itemType} types available.</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{items.map((item, index) => {
|
||||
const typeName = (item || {})[typeField] || ''
|
||||
const fieldGroup = fieldGroups.find(g => g.name === typeName)
|
||||
const config = parseConfig(item)
|
||||
return (
|
||||
<div key={index} className="card" style={{ marginBottom: 'var(--spacing-md)', padding: 'var(--spacing-md)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--spacing-md)' }}>
|
||||
<h4 style={{ margin: 0, fontWeight: 600 }}>{label} #{index + 1}</h4>
|
||||
<button type="button" className="btn btn-danger btn-sm" onClick={() => onRemove(index)}>
|
||||
<i className="fas fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">{label} Type</label>
|
||||
<select className="input" value={typeName} onChange={(e) => onChange(index, { ...items[index], [typeField]: e.target.value, config: '{}' })}>
|
||||
{typeOptions.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
{fieldGroup?.fields?.map(f => {
|
||||
const val = config[f.name] ?? ''
|
||||
const fieldLabel = <>{f.label}{f.required && <span style={{ color: 'var(--color-error)' }}> *</span>}</>
|
||||
if (f.type === 'checkbox') {
|
||||
return (
|
||||
<SettingRow key={f.name} label={fieldLabel} description={f.helpText}>
|
||||
<Toggle checked={val === 'true' || val === true} onChange={(v) => handleConfigFieldChange(index, f.name, v, 'checkbox')} />
|
||||
</SettingRow>
|
||||
)
|
||||
}
|
||||
if (f.type === 'textarea') {
|
||||
return (
|
||||
<div key={f.name} style={{ padding: 'var(--spacing-sm) 0', borderBottom: '1px solid var(--color-border-subtle)' }}>
|
||||
<div style={{ fontSize: '0.875rem', fontWeight: 500, marginBottom: 4 }}>{fieldLabel}</div>
|
||||
{f.helpText && <div style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)', marginBottom: 'var(--spacing-xs)' }}>{f.helpText}</div>}
|
||||
<textarea className="textarea" value={val} onChange={(e) => handleConfigFieldChange(index, f.name, e.target.value, 'text')} rows={3} placeholder={f.placeholder} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (f.type === 'select') {
|
||||
return (
|
||||
<SettingRow key={f.name} label={fieldLabel} description={f.helpText}>
|
||||
<select className="input" style={{ width: 200 }} value={val} onChange={(e) => handleConfigFieldChange(index, f.name, e.target.value, 'text')}>
|
||||
<option value="">— Select —</option>
|
||||
{(f.options || []).map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
</SettingRow>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<SettingRow key={f.name} label={fieldLabel} description={f.helpText}>
|
||||
<input
|
||||
className="input" type={f.type === 'number' ? 'number' : f.type === 'password' ? 'password' : 'text'}
|
||||
style={{ width: f.type === 'number' ? 120 : 200 }}
|
||||
value={val} onChange={(e) => handleConfigFieldChange(index, f.name, e.target.value, f.type)}
|
||||
placeholder={f.placeholder} min={f.min} max={f.max} step={f.step}
|
||||
/>
|
||||
</SettingRow>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<button type="button" className="btn btn-secondary" onClick={onAdd}>
|
||||
<i className="fas fa-plus" /> {addButtonText}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Section definitions ---
|
||||
|
||||
const SECTIONS = [
|
||||
{ id: 'BasicInfo', icon: 'fa-info-circle', label: 'Basic Info' },
|
||||
{ id: 'ModelSettings', icon: 'fa-brain', label: 'Model Settings' },
|
||||
{ id: 'MemorySettings', icon: 'fa-database', label: 'Memory' },
|
||||
{ id: 'PromptsGoals', icon: 'fa-bullseye', label: 'Prompts & Goals' },
|
||||
{ id: 'AdvancedSettings', icon: 'fa-cog', label: 'Advanced' },
|
||||
{ id: 'MCP', icon: 'fa-server', label: 'MCP Servers' },
|
||||
{ id: 'connectors', icon: 'fa-plug', label: 'Connectors' },
|
||||
{ id: 'actions', icon: 'fa-bolt', label: 'Actions' },
|
||||
{ id: 'filters', icon: 'fa-filter', label: 'Filters' },
|
||||
{ id: 'dynamic_prompts', icon: 'fa-wand-magic-sparkles', label: 'Dynamic Prompts' },
|
||||
]
|
||||
|
||||
// Fields handled by custom editors in the MCP section
|
||||
const CUSTOM_FIELDS = new Set(['mcp_stdio_servers'])
|
||||
|
||||
// --- Main component ---
|
||||
|
||||
export default function AgentCreate() {
|
||||
const { name } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { addToast } = useOutletContext()
|
||||
const isEdit = !!name
|
||||
const importedConfig = location.state?.importedConfig || null
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [activeSection, setActiveSection] = useState('BasicInfo')
|
||||
const [meta, setMeta] = useState(null)
|
||||
const [form, setForm] = useState({})
|
||||
const [connectors, setConnectors] = useState([])
|
||||
const [actions, setActions] = useState([])
|
||||
const [filters, setFilters] = useState([])
|
||||
const [dynamicPrompts, setDynamicPrompts] = useState([])
|
||||
const [mcpHttpServers, setMcpHttpServers] = useState([])
|
||||
const [stdioServers, setStdioServers] = useState([])
|
||||
|
||||
// Group metadata Fields by tags.section
|
||||
const fieldsBySection = useMemo(() => {
|
||||
if (!meta?.Fields) return {}
|
||||
const groups = {}
|
||||
for (const field of meta.Fields) {
|
||||
if (CUSTOM_FIELDS.has(field.name)) continue
|
||||
const section = field.tags?.section || 'BasicInfo'
|
||||
if (!groups[section]) groups[section] = []
|
||||
groups[section].push(field)
|
||||
}
|
||||
return groups
|
||||
}, [meta])
|
||||
|
||||
const visibleSections = useMemo(() => {
|
||||
const items = [...SECTIONS]
|
||||
if (isEdit) items.push({ id: 'export', icon: 'fa-download', label: 'Export' })
|
||||
return items
|
||||
}, [isEdit])
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
try {
|
||||
const [metaData, config] = await Promise.all([
|
||||
agentsApi.configMeta().catch(() => null),
|
||||
isEdit ? agentsApi.getConfig(name).catch(() => null) : Promise.resolve(null),
|
||||
])
|
||||
if (metaData) setMeta(metaData)
|
||||
|
||||
// Build defaults from metadata
|
||||
const initialForm = {}
|
||||
if (metaData?.Fields) {
|
||||
for (const field of metaData.Fields) {
|
||||
if (CUSTOM_FIELDS.has(field.name)) continue
|
||||
if (field.type === 'checkbox') {
|
||||
initialForm[field.name] = field.defaultValue != null ? !!field.defaultValue : false
|
||||
} else {
|
||||
initialForm[field.name] = field.defaultValue != null ? field.defaultValue : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Override with existing config when editing or importing
|
||||
const sourceConfig = config || importedConfig
|
||||
if (sourceConfig) {
|
||||
for (const key of Object.keys(initialForm)) {
|
||||
if (sourceConfig[key] !== undefined && sourceConfig[key] !== null) {
|
||||
initialForm[key] = sourceConfig[key]
|
||||
}
|
||||
}
|
||||
if (!initialForm.name && name) initialForm.name = name
|
||||
setConnectors(Array.isArray(sourceConfig.connectors) ? sourceConfig.connectors : [])
|
||||
setActions(Array.isArray(sourceConfig.actions) ? sourceConfig.actions : [])
|
||||
setFilters(Array.isArray(sourceConfig.filters) ? sourceConfig.filters : [])
|
||||
setDynamicPrompts(Array.isArray(sourceConfig.dynamic_prompts) ? sourceConfig.dynamic_prompts : [])
|
||||
setMcpHttpServers(Array.isArray(sourceConfig.mcp_servers) ? sourceConfig.mcp_servers : [])
|
||||
setStdioServers(parseStdioServers(sourceConfig.mcp_stdio_servers))
|
||||
}
|
||||
|
||||
setForm(initialForm)
|
||||
} catch (err) {
|
||||
addToast(`Failed to load configuration: ${err.message}`, 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
init()
|
||||
}, [name, isEdit, importedConfig, addToast])
|
||||
|
||||
const updateField = (fieldName, value) => {
|
||||
setForm(prev => ({ ...prev, [fieldName]: value }))
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!form.name?.toString().trim()) {
|
||||
addToast('Agent name is required', 'warning')
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
try {
|
||||
const payload = { ...form }
|
||||
// Convert number fields
|
||||
if (meta?.Fields) {
|
||||
for (const field of meta.Fields) {
|
||||
if (field.type === 'number' && payload[field.name] !== '' && payload[field.name] != null) {
|
||||
payload[field.name] = Number(payload[field.name])
|
||||
}
|
||||
}
|
||||
}
|
||||
payload.connectors = connectors
|
||||
payload.actions = actions
|
||||
payload.filters = filters
|
||||
payload.dynamic_prompts = dynamicPrompts
|
||||
payload.mcp_servers = mcpHttpServers.filter(s => s.url)
|
||||
// Send STDIO servers as JSON string in expected format
|
||||
if (stdioServers.length > 0) {
|
||||
payload.mcp_stdio_servers = buildStdioJson(stdioServers)
|
||||
}
|
||||
|
||||
if (isEdit) {
|
||||
await agentsApi.update(name, payload)
|
||||
addToast(`Agent "${form.name}" updated`, 'success')
|
||||
} else {
|
||||
await agentsApi.create(payload)
|
||||
addToast(`Agent "${form.name}" created`, 'success')
|
||||
}
|
||||
navigate('/agents')
|
||||
} catch (err) {
|
||||
addToast(`Save failed: ${err.message}`, 'error')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
// --- STDIO server handlers ---
|
||||
const addStdioServer = () => setStdioServers(prev => [...prev, { name: '', command: '', args: [], env: [] }])
|
||||
const removeStdioServer = (idx) => setStdioServers(prev => prev.filter((_, i) => i !== idx))
|
||||
const updateStdio = (idx, key, val) => setStdioServers(prev => { const n = [...prev]; n[idx] = { ...n[idx], [key]: val }; return n })
|
||||
const addArg = (si) => setStdioServers(prev => { const n = [...prev]; n[si] = { ...n[si], args: [...(n[si].args || []), ''] }; return n })
|
||||
const updateArg = (si, ai, val) => setStdioServers(prev => { const n = [...prev]; const a = [...(n[si].args || [])]; a[ai] = val; n[si] = { ...n[si], args: a }; return n })
|
||||
const removeArg = (si, ai) => setStdioServers(prev => { const n = [...prev]; n[si] = { ...n[si], args: n[si].args.filter((_, i) => i !== ai) }; return n })
|
||||
const addEnv = (si) => setStdioServers(prev => { const n = [...prev]; n[si] = { ...n[si], env: [...(n[si].env || []), ''] }; return n })
|
||||
const updateEnv = (si, ei, val) => setStdioServers(prev => { const n = [...prev]; const e = [...(n[si].env || [])]; e[ei] = val; n[si] = { ...n[si], env: e }; return n })
|
||||
const removeEnv = (si, ei) => setStdioServers(prev => { const n = [...prev]; n[si] = { ...n[si], env: n[si].env.filter((_, i) => i !== ei) }; return n })
|
||||
|
||||
// --- HTTP MCP server handlers ---
|
||||
const addMcpHttp = () => setMcpHttpServers(prev => [...prev, { url: '', token: '' }])
|
||||
const removeMcpHttp = (idx) => setMcpHttpServers(prev => prev.filter((_, i) => i !== idx))
|
||||
const updateMcpHttp = (idx, key, val) => setMcpHttpServers(prev => { const n = [...prev]; n[idx] = { ...n[idx], [key]: val }; return n })
|
||||
|
||||
// --- Render helpers ---
|
||||
|
||||
const renderFieldSection = (sectionId) => {
|
||||
const fields = fieldsBySection[sectionId] || []
|
||||
if (!fields.length) {
|
||||
return <p style={{ color: 'var(--color-text-muted)', fontSize: '0.875rem' }}>No fields available for this section.</p>
|
||||
}
|
||||
return fields.map(field => (
|
||||
<FormField
|
||||
key={field.name}
|
||||
field={field.name === 'name' && isEdit ? { ...field, disabled: true, helpText: 'Agent name cannot be changed after creation' } : field}
|
||||
value={form[field.name]}
|
||||
onChange={updateField}
|
||||
disabled={field.name === 'name' && isEdit}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
const renderSection = () => {
|
||||
switch (activeSection) {
|
||||
case 'BasicInfo':
|
||||
case 'ModelSettings':
|
||||
case 'MemorySettings':
|
||||
case 'PromptsGoals':
|
||||
case 'AdvancedSettings':
|
||||
return renderFieldSection(activeSection)
|
||||
|
||||
case 'MCP':
|
||||
return (
|
||||
<>
|
||||
{/* Other MCP metadata fields (mcp_prepare_script, etc.) */}
|
||||
{renderFieldSection('MCP')}
|
||||
|
||||
{/* STDIO Servers */}
|
||||
<div style={{ marginTop: 'var(--spacing-lg)' }}>
|
||||
<h4 className="agent-subsection-title">
|
||||
<i className="fas fa-terminal" style={{ color: 'var(--color-primary)', marginRight: 'var(--spacing-xs)' }} />
|
||||
STDIO Servers
|
||||
</h4>
|
||||
<p className="agent-section-desc">Local command-based MCP servers (e.g. docker run).</p>
|
||||
{stdioServers.map((server, idx) => (
|
||||
<div key={idx} className="card" style={{ marginBottom: 'var(--spacing-md)', padding: 'var(--spacing-md)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--spacing-sm)' }}>
|
||||
<span style={{ fontWeight: 600, fontSize: '0.85rem' }}>Server #{idx + 1}</span>
|
||||
<button type="button" className="btn btn-danger btn-sm" onClick={() => removeStdioServer(idx)}>
|
||||
<i className="fas fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--spacing-sm)' }}>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Name</label>
|
||||
<input className="input" value={server.name || ''} onChange={(e) => updateStdio(idx, 'name', e.target.value)} placeholder="server-name" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Command</label>
|
||||
<input className="input" value={server.command || ''} onChange={(e) => updateStdio(idx, 'command', e.target.value)} placeholder="/usr/bin/node" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
Arguments
|
||||
<button type="button" className="btn btn-secondary btn-sm" onClick={() => addArg(idx)} style={{ fontSize: '0.7rem', padding: '2px 8px' }}>
|
||||
<i className="fas fa-plus" /> Add
|
||||
</button>
|
||||
</label>
|
||||
{(server.args || []).length === 0 && <p style={{ fontSize: '0.8rem', color: 'var(--color-text-muted)' }}>No arguments.</p>}
|
||||
{(server.args || []).map((arg, ai) => (
|
||||
<div key={ai} style={{ display: 'flex', gap: 'var(--spacing-xs)', marginBottom: 'var(--spacing-xs)' }}>
|
||||
<input className="input" value={arg} onChange={(e) => updateArg(idx, ai, e.target.value)} placeholder="argument" style={{ flex: 1 }} />
|
||||
<button type="button" className="btn btn-danger btn-sm" onClick={() => removeArg(idx, ai)}><i className="fas fa-times" /></button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
Environment Variables
|
||||
<button type="button" className="btn btn-secondary btn-sm" onClick={() => addEnv(idx)} style={{ fontSize: '0.7rem', padding: '2px 8px' }}>
|
||||
<i className="fas fa-plus" /> Add
|
||||
</button>
|
||||
</label>
|
||||
{(server.env || []).length === 0 && <p style={{ fontSize: '0.8rem', color: 'var(--color-text-muted)' }}>No environment variables.</p>}
|
||||
{(server.env || []).map((env, ei) => (
|
||||
<div key={ei} style={{ display: 'flex', gap: 'var(--spacing-xs)', marginBottom: 'var(--spacing-xs)' }}>
|
||||
<input className="input" value={env} onChange={(e) => updateEnv(idx, ei, e.target.value)} placeholder="KEY=VALUE" style={{ flex: 1 }} />
|
||||
<button type="button" className="btn btn-danger btn-sm" onClick={() => removeEnv(idx, ei)}><i className="fas fa-times" /></button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button type="button" className="btn btn-secondary" onClick={addStdioServer}>
|
||||
<i className="fas fa-plus" /> Add STDIO Server
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* HTTP Servers */}
|
||||
<div style={{ marginTop: 'var(--spacing-lg)' }}>
|
||||
<h4 className="agent-subsection-title">
|
||||
<i className="fas fa-globe" style={{ color: 'var(--color-primary)', marginRight: 'var(--spacing-xs)' }} />
|
||||
HTTP Servers
|
||||
</h4>
|
||||
<p className="agent-section-desc">MCP servers connected over HTTP.</p>
|
||||
{mcpHttpServers.map((server, idx) => (
|
||||
<div key={idx} className="card" style={{ marginBottom: 'var(--spacing-md)', padding: 'var(--spacing-md)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--spacing-sm)' }}>
|
||||
<span style={{ fontWeight: 600, fontSize: '0.85rem' }}>HTTP Server #{idx + 1}</span>
|
||||
<button type="button" className="btn btn-danger btn-sm" onClick={() => removeMcpHttp(idx)}>
|
||||
<i className="fas fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
{(meta?.MCPServers || [{ name: 'url', label: 'URL', type: 'text' }, { name: 'token', label: 'API Key', type: 'password' }]).map(f => (
|
||||
<div key={f.name} className="form-group">
|
||||
<label className="form-label">{f.label}{f.required && <span style={{ color: 'var(--color-error)' }}> *</span>}</label>
|
||||
<input
|
||||
className="input" type={f.type === 'password' ? 'password' : 'text'}
|
||||
value={server[f.name] || ''} onChange={(e) => updateMcpHttp(idx, f.name, e.target.value)}
|
||||
placeholder={f.placeholder}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<button type="button" className="btn btn-secondary" onClick={addMcpHttp}>
|
||||
<i className="fas fa-plus" /> Add HTTP Server
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
case 'connectors':
|
||||
return (
|
||||
<>
|
||||
<p className="agent-section-desc">Configure connectors that this agent uses to communicate with external services.</p>
|
||||
<ConfigForm
|
||||
items={connectors}
|
||||
fieldGroups={meta?.Connectors}
|
||||
onChange={(idx, item) => { const n = [...connectors]; n[idx] = item; setConnectors(n) }}
|
||||
onRemove={(idx) => setConnectors(connectors.filter((_, i) => i !== idx))}
|
||||
onAdd={() => setConnectors([...connectors, { type: '', config: '{}' }])}
|
||||
typeField="type" itemType="connector" addButtonText="Add Connector"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
case 'actions':
|
||||
return (
|
||||
<>
|
||||
<p className="agent-section-desc">Configure actions the agent can perform.</p>
|
||||
<ConfigForm
|
||||
items={actions}
|
||||
fieldGroups={meta?.Actions}
|
||||
onChange={(idx, item) => { const n = [...actions]; n[idx] = item; setActions(n) }}
|
||||
onRemove={(idx) => setActions(actions.filter((_, i) => i !== idx))}
|
||||
onAdd={() => setActions([...actions, { name: '', config: '{}' }])}
|
||||
typeField="name" itemType="action" addButtonText="Add Action"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
case 'filters':
|
||||
return (
|
||||
<>
|
||||
<p className="agent-section-desc">Filters and triggers that control which messages the agent processes.</p>
|
||||
<ConfigForm
|
||||
items={filters}
|
||||
fieldGroups={meta?.Filters}
|
||||
onChange={(idx, item) => { const n = [...filters]; n[idx] = item; setFilters(n) }}
|
||||
onRemove={(idx) => setFilters(filters.filter((_, i) => i !== idx))}
|
||||
onAdd={() => setFilters([...filters, { type: '', config: '{}' }])}
|
||||
typeField="type" itemType="filter" addButtonText="Add Filter"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
case 'dynamic_prompts':
|
||||
return (
|
||||
<>
|
||||
<p className="agent-section-desc">Dynamic prompts that augment agent context at runtime.</p>
|
||||
<ConfigForm
|
||||
items={dynamicPrompts}
|
||||
fieldGroups={meta?.DynamicPrompts}
|
||||
onChange={(idx, item) => { const n = [...dynamicPrompts]; n[idx] = item; setDynamicPrompts(n) }}
|
||||
onRemove={(idx) => setDynamicPrompts(dynamicPrompts.filter((_, i) => i !== idx))}
|
||||
onAdd={() => setDynamicPrompts([...dynamicPrompts, { type: '', config: '{}' }])}
|
||||
typeField="type" itemType="dynamic prompt" addButtonText="Add Dynamic Prompt"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
case 'export':
|
||||
return (
|
||||
<div>
|
||||
<p className="agent-section-desc">Download the full agent configuration as a JSON file.</p>
|
||||
<a
|
||||
href={`/api/agents/${encodeURIComponent(name)}/export`}
|
||||
className="btn btn-primary"
|
||||
style={{ display: 'inline-flex', alignItems: 'center', textDecoration: 'none' }}
|
||||
>
|
||||
<i className="fas fa-download" style={{ marginRight: 'var(--spacing-xs)' }} /> Export Agent
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="page" style={{ display: 'flex', justifyContent: 'center', padding: 'var(--spacing-xl)' }}>
|
||||
<i className="fas fa-spinner fa-spin" style={{ fontSize: '2rem', color: 'var(--color-primary)' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<style>{`
|
||||
.agent-form-container {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
min-height: 500px;
|
||||
}
|
||||
.agent-wizard-sidebar {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.agent-wizard-nav {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: sticky;
|
||||
top: var(--spacing-md);
|
||||
}
|
||||
.agent-wizard-nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
transition: background 0.15s, color 0.15s;
|
||||
user-select: none;
|
||||
margin-bottom: 2px;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
.agent-wizard-nav-item:hover {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.agent-wizard-nav-item.active {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
border-left-color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
.agent-wizard-nav-item i {
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.agent-wizard-badge {
|
||||
margin-left: auto;
|
||||
font-size: 0.7rem;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border-radius: 999px;
|
||||
padding: 1px 6px;
|
||||
min-width: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
.agent-form-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.agent-section-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: var(--spacing-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
.agent-subsection-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
.agent-section-desc {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
.agent-form-help-text {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: var(--spacing-xs);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.agent-form-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
.agent-wizard-sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
.agent-wizard-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-xs);
|
||||
position: static;
|
||||
}
|
||||
.agent-wizard-nav-item {
|
||||
font-size: 0.8125rem;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-left: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
}
|
||||
.agent-wizard-nav-item.active {
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h1 className="page-title">{isEdit ? `Edit Agent: ${name}` : importedConfig ? 'Import Agent' : 'Create Agent'}</h1>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => navigate('/agents')}>
|
||||
<i className="fas fa-arrow-left" /> Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} noValidate>
|
||||
<div className="agent-form-container">
|
||||
<div className="agent-wizard-sidebar">
|
||||
<div className="card" style={{ padding: 'var(--spacing-sm)' }}>
|
||||
<ul className="agent-wizard-nav">
|
||||
{visibleSections.map(s => {
|
||||
let count = 0
|
||||
if (s.id === 'connectors') count = connectors.length
|
||||
else if (s.id === 'actions') count = actions.length
|
||||
else if (s.id === 'filters') count = filters.length
|
||||
else if (s.id === 'dynamic_prompts') count = dynamicPrompts.length
|
||||
return (
|
||||
<li
|
||||
key={s.id}
|
||||
className={`agent-wizard-nav-item ${activeSection === s.id ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection(s.id)}
|
||||
>
|
||||
<i className={`fas ${s.icon}`} />
|
||||
{s.label}
|
||||
{count > 0 && <span className="agent-wizard-badge">{count}</span>}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="agent-form-content">
|
||||
<div className="card" style={{ padding: 'var(--spacing-lg)' }}>
|
||||
<h3 className="agent-section-title">
|
||||
<i className={`fas ${visibleSections.find(s => s.id === activeSection)?.icon || 'fa-cog'}`} style={{ color: 'var(--color-primary)' }} />
|
||||
{visibleSections.find(s => s.id === activeSection)?.label || activeSection}
|
||||
</h3>
|
||||
{renderSection()}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)', justifyContent: 'flex-end', marginTop: 'var(--spacing-md)' }}>
|
||||
<button type="button" className="btn btn-secondary" onClick={() => navigate('/agents')}>
|
||||
<i className="fas fa-times" /> Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={saving}>
|
||||
{saving
|
||||
? <><i className="fas fa-spinner fa-spin" /> Saving...</>
|
||||
: <><i className="fas fa-save" /> {isEdit ? 'Save Changes' : importedConfig ? 'Import Agent' : 'Create Agent'}</>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
418
core/http/react-ui/src/pages/AgentStatus.jsx
Normal file
418
core/http/react-ui/src/pages/AgentStatus.jsx
Normal file
@@ -0,0 +1,418 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useParams, useNavigate, useOutletContext } from 'react-router-dom'
|
||||
import { agentsApi } from '../utils/api'
|
||||
|
||||
function ObservableSummary({ observable }) {
|
||||
const creation = observable?.creation || {}
|
||||
const completion = observable?.completion || {}
|
||||
|
||||
let creationMsg = ''
|
||||
if (creation?.chat_completion_message?.content) {
|
||||
creationMsg = creation.chat_completion_message.content
|
||||
} else {
|
||||
const messages = creation?.chat_completion_request?.messages
|
||||
if (Array.isArray(messages) && messages.length > 0) {
|
||||
creationMsg = messages[messages.length - 1]?.content || ''
|
||||
}
|
||||
}
|
||||
if (typeof creationMsg === 'object') creationMsg = 'Multimedia message'
|
||||
|
||||
let funcDef = creation?.function_definition?.name ? `Function: ${creation.function_definition.name}` : ''
|
||||
let funcParams = creation?.function_params && Object.keys(creation.function_params).length > 0
|
||||
? `Params: ${JSON.stringify(creation.function_params)}` : ''
|
||||
|
||||
let completionMsg = ''
|
||||
let toolCallSummary = ''
|
||||
let chatCompletion = completion?.chat_completion_response
|
||||
if (!chatCompletion && Array.isArray(completion?.conversation) && completion.conversation.length > 0) {
|
||||
chatCompletion = { choices: completion.conversation.map(m => ({ message: m })) }
|
||||
}
|
||||
if (chatCompletion?.choices?.length > 0) {
|
||||
const last = chatCompletion.choices[chatCompletion.choices.length - 1]
|
||||
const toolCalls = last?.message?.tool_calls
|
||||
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
||||
toolCallSummary = toolCalls.map(tc => {
|
||||
const args = tc.function?.arguments || ''
|
||||
return `${tc.function?.name || 'unknown'}(${typeof args === 'string' ? args : JSON.stringify(args)})`
|
||||
}).join(', ')
|
||||
}
|
||||
completionMsg = last?.message?.content || ''
|
||||
}
|
||||
|
||||
let actionResult = completion?.action_result ? String(completion.action_result).slice(0, 100) : ''
|
||||
let errorMsg = completion?.error || ''
|
||||
let filterInfo = ''
|
||||
if (completion?.filter_result) {
|
||||
const fr = completion.filter_result
|
||||
if (fr.has_triggers && !fr.triggered_by) filterInfo = 'Failed to match triggers'
|
||||
else if (fr.triggered_by) filterInfo = `Triggered by ${fr.triggered_by}`
|
||||
if (fr.failed_by) filterInfo += `${filterInfo ? ', ' : ''}Failed by ${fr.failed_by}`
|
||||
}
|
||||
|
||||
const items = []
|
||||
if (creationMsg) items.push({ icon: 'fa-comment-dots', text: creationMsg, cls: 'creation' })
|
||||
if (funcDef) items.push({ icon: 'fa-code', text: funcDef, cls: 'creation' })
|
||||
if (funcParams) items.push({ icon: 'fa-sliders-h', text: funcParams, cls: 'creation' })
|
||||
if (toolCallSummary) items.push({ icon: 'fa-wrench', text: toolCallSummary, cls: 'tool-call' })
|
||||
if (completionMsg) items.push({ icon: 'fa-robot', text: completionMsg, cls: 'completion' })
|
||||
if (actionResult) items.push({ icon: 'fa-bolt', text: actionResult, cls: 'tool-call' })
|
||||
if (errorMsg) items.push({ icon: 'fa-exclamation-triangle', text: errorMsg, cls: 'error' })
|
||||
if (filterInfo) items.push({ icon: 'fa-shield-alt', text: filterInfo, cls: 'completion' })
|
||||
|
||||
if (items.length === 0) return null
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, marginTop: 2 }}>
|
||||
{items.map((item, i) => (
|
||||
<div key={i} className={`as-summary-item as-summary-${item.cls}`} title={item.text}>
|
||||
<i className={`fas ${item.icon}`} />
|
||||
<span>{item.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ObservableCard({ observable, children: childNodes }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const isComplete = !!observable.completion
|
||||
const hasProgress = observable.progress?.length > 0
|
||||
|
||||
return (
|
||||
<div className="as-card">
|
||||
<div className="as-card-header" onClick={() => setExpanded(!expanded)}>
|
||||
<div className="as-card-title">
|
||||
<div className="as-obs-icon">
|
||||
<i className={`fas fa-${observable.icon || 'robot'}`} />
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-xs)' }}>
|
||||
<span style={{ fontWeight: 600, fontSize: '0.875rem' }}>{observable.name}</span>
|
||||
<span className="as-id">#{observable.id}</span>
|
||||
{!isComplete && <i className="fas fa-circle-notch fa-spin" style={{ fontSize: '0.7rem', color: 'var(--color-primary)' }} />}
|
||||
</div>
|
||||
<ObservableSummary observable={observable} />
|
||||
</div>
|
||||
</div>
|
||||
<i className={`fas fa-chevron-${expanded ? 'up' : 'down'}`} style={{ color: 'var(--color-text-muted)', fontSize: '0.75rem' }} />
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="as-card-body">
|
||||
{/* Children (nested observables) */}
|
||||
{childNodes && childNodes.length > 0 && (
|
||||
<div style={{ marginBottom: 'var(--spacing-md)' }}>
|
||||
<div style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--color-text-muted)', textTransform: 'uppercase', marginBottom: 'var(--spacing-xs)' }}>
|
||||
Nested Observables
|
||||
</div>
|
||||
{childNodes}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress entries */}
|
||||
{hasProgress && (
|
||||
<div style={{ marginBottom: 'var(--spacing-sm)' }}>
|
||||
<div className="as-section-label">Progress ({observable.progress.length})</div>
|
||||
{observable.progress.map((p, i) => (
|
||||
<div key={i} className="as-progress-entry">
|
||||
{p.action_result && <div><span className="as-tag">Action Result</span> {p.action_result}</div>}
|
||||
{p.error && <div className="as-error-text"><span className="as-tag as-tag-error">Error</span> {p.error}</div>}
|
||||
{p.chat_completion_response?.choices?.length > 0 && (
|
||||
<div>
|
||||
<span className="as-tag">Response</span>{' '}
|
||||
{p.chat_completion_response.choices.map((ch, ci) => (
|
||||
<span key={ci}>{ch.message?.content || '(tool call)'}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{p.agent_state && (
|
||||
<div><span className="as-tag">State</span> {JSON.stringify(p.agent_state)}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Completion */}
|
||||
{observable.completion && (
|
||||
<div style={{ marginBottom: 'var(--spacing-sm)' }}>
|
||||
<div className="as-section-label">Completion</div>
|
||||
{observable.completion.action_result && (
|
||||
<div className="as-progress-entry"><span className="as-tag">Action Result</span> {observable.completion.action_result}</div>
|
||||
)}
|
||||
{observable.completion.error && (
|
||||
<div className="as-progress-entry as-error-text"><span className="as-tag as-tag-error">Error</span> {observable.completion.error}</div>
|
||||
)}
|
||||
{observable.completion.filter_result && (
|
||||
<div className="as-progress-entry"><span className="as-tag">Filter</span> {JSON.stringify(observable.completion.filter_result)}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Raw JSON */}
|
||||
<details className="as-raw">
|
||||
<summary>Raw JSON</summary>
|
||||
<pre className="as-json">{JSON.stringify(observable, null, 2)}</pre>
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function buildTree(observables) {
|
||||
const byId = {}
|
||||
observables.forEach(obs => { byId[obs.id] = { ...obs, children: [] } })
|
||||
const roots = []
|
||||
observables.forEach(obs => {
|
||||
if (obs.parent_id && byId[obs.parent_id]) {
|
||||
byId[obs.parent_id].children.push(byId[obs.id])
|
||||
} else {
|
||||
roots.push(byId[obs.id])
|
||||
}
|
||||
})
|
||||
return roots
|
||||
}
|
||||
|
||||
function renderTree(nodes) {
|
||||
return nodes.map(node => (
|
||||
<ObservableCard key={node.id} observable={node}>
|
||||
{node.children.length > 0 ? renderTree(node.children) : null}
|
||||
</ObservableCard>
|
||||
))
|
||||
}
|
||||
|
||||
export default function AgentStatus() {
|
||||
const { name } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { addToast } = useOutletContext()
|
||||
const [observables, setObservables] = useState([])
|
||||
const [status, setStatus] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const obsData = await agentsApi.observables(name)
|
||||
const history = Array.isArray(obsData) ? obsData : (obsData?.History || [])
|
||||
setObservables(history)
|
||||
} catch (err) {
|
||||
addToast(`Failed to load observables: ${err.message}`, 'error')
|
||||
}
|
||||
try {
|
||||
const statusData = await agentsApi.status(name)
|
||||
setStatus(statusData)
|
||||
} catch (_) {
|
||||
// status endpoint may fail if no actions have run yet
|
||||
}
|
||||
setLoading(false)
|
||||
}, [name, addToast])
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
const interval = setInterval(fetchData, 5000)
|
||||
return () => clearInterval(interval)
|
||||
}, [fetchData])
|
||||
|
||||
// SSE for real-time observable updates
|
||||
useEffect(() => {
|
||||
const url = `/api/agents/${encodeURIComponent(name)}/sse`
|
||||
const es = new EventSource(url)
|
||||
|
||||
es.addEventListener('observable_update', (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data)
|
||||
setObservables(prev => {
|
||||
const idx = prev.findIndex(o => o.id === data.id)
|
||||
if (idx >= 0) {
|
||||
const updated = [...prev]
|
||||
const existing = updated[idx]
|
||||
updated[idx] = {
|
||||
...existing,
|
||||
...data,
|
||||
creation: data.creation || existing.creation,
|
||||
completion: data.completion || existing.completion,
|
||||
progress: (data.progress?.length ?? 0) > (existing.progress?.length ?? 0) ? data.progress : existing.progress,
|
||||
}
|
||||
return updated
|
||||
}
|
||||
return [...prev, data]
|
||||
})
|
||||
} catch (_) { /* ignore */ }
|
||||
})
|
||||
|
||||
es.onerror = () => { /* reconnect handled by browser */ }
|
||||
return () => es.close()
|
||||
}, [name])
|
||||
|
||||
const handleClear = async () => {
|
||||
try {
|
||||
await agentsApi.clearObservables(name)
|
||||
setObservables([])
|
||||
addToast('Observables cleared', 'success')
|
||||
} catch (err) {
|
||||
addToast(`Failed to clear: ${err.message}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const tree = buildTree(observables)
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<style>{`
|
||||
.as-card {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
.as-card .as-card {
|
||||
border-left: 3px solid var(--color-primary);
|
||||
margin-left: var(--spacing-md);
|
||||
}
|
||||
.as-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px var(--spacing-md);
|
||||
cursor: pointer;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
.as-card-header:hover { background: var(--color-bg-tertiary); }
|
||||
.as-card-title { display: flex; align-items: flex-start; gap: var(--spacing-sm); flex: 1; min-width: 0; }
|
||||
.as-obs-icon {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.75rem; flex-shrink: 0;
|
||||
}
|
||||
.as-id {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-text-muted);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.as-summary-item {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
font-size: 0.75rem; color: var(--color-text-secondary);
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.as-summary-item i { font-size: 0.625rem; flex-shrink: 0; }
|
||||
.as-summary-creation i { color: var(--color-primary); }
|
||||
.as-summary-tool-call i { color: #f59e0b; }
|
||||
.as-summary-completion i { color: var(--color-success); }
|
||||
.as-summary-error i { color: var(--color-error); }
|
||||
.as-card-body {
|
||||
padding: var(--spacing-md);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.as-section-label {
|
||||
font-size: 0.6875rem; font-weight: 600; text-transform: uppercase;
|
||||
letter-spacing: 0.04em; color: var(--color-text-muted);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
.as-progress-entry {
|
||||
font-size: 0.8125rem; color: var(--color-text-primary);
|
||||
padding: 4px 0; border-bottom: 1px solid var(--color-border-subtle);
|
||||
word-break: break-word;
|
||||
}
|
||||
.as-progress-entry:last-child { border-bottom: none; }
|
||||
.as-tag {
|
||||
display: inline-block; padding: 1px 6px; border-radius: var(--radius-sm);
|
||||
font-size: 0.625rem; font-weight: 600; text-transform: uppercase;
|
||||
background: var(--color-bg-tertiary); color: var(--color-text-muted);
|
||||
margin-right: 4px; vertical-align: middle;
|
||||
}
|
||||
.as-tag-error { background: var(--color-error); color: #fff; }
|
||||
.as-error-text { color: var(--color-error); }
|
||||
.as-raw { margin-top: var(--spacing-sm); }
|
||||
.as-raw summary { font-size: 0.75rem; color: var(--color-text-muted); cursor: pointer; }
|
||||
.as-json {
|
||||
background: var(--color-bg-tertiary); border-radius: var(--radius-sm);
|
||||
padding: var(--spacing-sm); font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.75rem; overflow-x: auto; white-space: pre-wrap;
|
||||
word-break: break-word; max-height: 300px; overflow-y: auto;
|
||||
}
|
||||
.as-status-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: var(--spacing-sm); margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
.as-status-item {
|
||||
background: var(--color-bg-secondary); border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md); padding: var(--spacing-md);
|
||||
}
|
||||
.as-status-label {
|
||||
font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.05em;
|
||||
color: var(--color-text-muted); margin-bottom: 4px;
|
||||
}
|
||||
.as-status-value { font-size: 1rem; font-weight: 600; color: var(--color-text-primary); }
|
||||
`}</style>
|
||||
|
||||
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<h1 className="page-title">
|
||||
<i className="fas fa-chart-bar" style={{ marginRight: 'var(--spacing-xs)' }} />
|
||||
{name} — Status
|
||||
</h1>
|
||||
<p className="page-subtitle">Agent observables and activity history</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)' }}>
|
||||
<button className="btn btn-secondary" onClick={() => navigate(`/agents/${encodeURIComponent(name)}/chat`)}>
|
||||
<i className="fas fa-comment" /> Chat
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => navigate(`/agents/${encodeURIComponent(name)}/edit`)}>
|
||||
<i className="fas fa-edit" /> Edit
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={fetchData}>
|
||||
<i className="fas fa-sync" /> Refresh
|
||||
</button>
|
||||
<button className="btn btn-danger" onClick={handleClear} disabled={observables.length === 0}>
|
||||
<i className="fas fa-trash" /> Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status summary */}
|
||||
{status && (
|
||||
<div className="as-status-grid">
|
||||
{status.state && (
|
||||
<div className="as-status-item">
|
||||
<div className="as-status-label">State</div>
|
||||
<div className="as-status-value">{status.state}</div>
|
||||
</div>
|
||||
)}
|
||||
{status.current_task && (
|
||||
<div className="as-status-item">
|
||||
<div className="as-status-label">Current Task</div>
|
||||
<div className="as-status-value" style={{ fontSize: '0.8125rem', fontWeight: 400 }}>{status.current_task}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="as-status-item">
|
||||
<div className="as-status-label">Observables</div>
|
||||
<div className="as-status-value">{observables.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--spacing-xl)' }}>
|
||||
<i className="fas fa-spinner fa-spin" style={{ fontSize: '2rem', color: 'var(--color-primary)' }} />
|
||||
</div>
|
||||
) : tree.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon"><i className="fas fa-chart-bar" /></div>
|
||||
<h2 className="empty-state-title">No observables yet</h2>
|
||||
<p className="empty-state-text">Send a message to the agent to see its activity here.</p>
|
||||
<button className="btn btn-primary" onClick={() => navigate(`/agents/${encodeURIComponent(name)}/chat`)}>
|
||||
<i className="fas fa-comment" /> Chat with {name}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{renderTree(tree)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
303
core/http/react-ui/src/pages/Agents.jsx
Normal file
303
core/http/react-ui/src/pages/Agents.jsx
Normal file
@@ -0,0 +1,303 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom'
|
||||
import { agentsApi } from '../utils/api'
|
||||
|
||||
export default function Agents() {
|
||||
const { addToast } = useOutletContext()
|
||||
const navigate = useNavigate()
|
||||
const [agents, setAgents] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [agentHubURL, setAgentHubURL] = useState('')
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const fetchAgents = useCallback(async () => {
|
||||
try {
|
||||
const data = await agentsApi.list()
|
||||
const names = Array.isArray(data.agents) ? data.agents : []
|
||||
const statuses = data.statuses || {}
|
||||
if (data.agent_hub_url) setAgentHubURL(data.agent_hub_url)
|
||||
setAgents(names.map(name => ({
|
||||
name,
|
||||
status: statuses[name] ? 'active' : 'paused',
|
||||
})))
|
||||
} catch (err) {
|
||||
addToast(`Failed to load agents: ${err.message}`, 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [addToast])
|
||||
|
||||
useEffect(() => {
|
||||
fetchAgents()
|
||||
const interval = setInterval(fetchAgents, 5000)
|
||||
return () => clearInterval(interval)
|
||||
}, [fetchAgents])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!search.trim()) return agents
|
||||
const q = search.toLowerCase()
|
||||
return agents.filter(a => a.name.toLowerCase().includes(q))
|
||||
}, [agents, search])
|
||||
|
||||
const handleDelete = async (name) => {
|
||||
if (!window.confirm(`Delete agent "${name}"? This action cannot be undone.`)) return
|
||||
try {
|
||||
await agentsApi.delete(name)
|
||||
addToast(`Agent "${name}" deleted`, 'success')
|
||||
fetchAgents()
|
||||
} catch (err) {
|
||||
addToast(`Failed to delete agent: ${err.message}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handlePauseResume = async (agent) => {
|
||||
const name = agent.name || agent.id
|
||||
const isActive = agent.status === 'active'
|
||||
try {
|
||||
if (isActive) {
|
||||
await agentsApi.pause(name)
|
||||
addToast(`Agent "${name}" paused`, 'success')
|
||||
} else {
|
||||
await agentsApi.resume(name)
|
||||
addToast(`Agent "${name}" resumed`, 'success')
|
||||
}
|
||||
fetchAgents()
|
||||
} catch (err) {
|
||||
addToast(`Failed to ${isActive ? 'pause' : 'resume'} agent: ${err.message}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleExport = async (name) => {
|
||||
try {
|
||||
const data = await agentsApi.export(name)
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${name}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
addToast(`Agent "${name}" exported`, 'success')
|
||||
} catch (err) {
|
||||
addToast(`Failed to export agent: ${err.message}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = async (e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
try {
|
||||
const text = await file.text()
|
||||
const config = JSON.parse(text)
|
||||
navigate('/agents/new', { state: { importedConfig: config } })
|
||||
} catch (err) {
|
||||
addToast(`Failed to parse agent file: ${err.message}`, 'error')
|
||||
}
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
const statusBadge = (status) => {
|
||||
const cls = status === 'active' ? 'badge-success' : status === 'paused' ? 'badge-warning' : ''
|
||||
return <span className={`badge ${cls}`}>{status || 'unknown'}</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<style>{`
|
||||
.agents-import-input { display: none; }
|
||||
.agents-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.agents-search {
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
max-width: 360px;
|
||||
position: relative;
|
||||
}
|
||||
.agents-search i {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.8125rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
.agents-search input {
|
||||
padding-left: 32px;
|
||||
}
|
||||
.agents-action-group {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.agents-name {
|
||||
cursor: pointer;
|
||||
color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
.agents-name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<h1 className="page-title">Agents</h1>
|
||||
<p className="page-subtitle">Manage autonomous AI agents</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)', alignItems: 'center' }}>
|
||||
{agentHubURL && (
|
||||
<a className="btn btn-secondary" href={agentHubURL} target="_blank" rel="noopener noreferrer">
|
||||
<i className="fas fa-store" /> Agent Hub
|
||||
</a>
|
||||
)}
|
||||
<label className="btn btn-secondary">
|
||||
<i className="fas fa-file-import" /> Import
|
||||
<input type="file" accept=".json" className="agents-import-input" onChange={handleImport} />
|
||||
</label>
|
||||
<button className="btn btn-primary" onClick={() => navigate('/agents/new')}>
|
||||
<i className="fas fa-plus" /> Create Agent
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--spacing-xl)' }}>
|
||||
<i className="fas fa-spinner fa-spin" style={{ fontSize: '2rem', color: 'var(--color-primary)' }} />
|
||||
</div>
|
||||
) : agents.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon"><i className="fas fa-robot" /></div>
|
||||
<h2 className="empty-state-title">No agents configured</h2>
|
||||
<p className="empty-state-text">Create an agent to get started with autonomous AI workflows.</p>
|
||||
{agentHubURL && (
|
||||
<p className="empty-state-text">
|
||||
Don't know where to start? Browse the <a href={agentHubURL} target="_blank" rel="noopener noreferrer">Agent Hub</a> to find ready-made agent configurations you can import.
|
||||
</p>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)', justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||
<button className="btn btn-primary" onClick={() => navigate('/agents/new')}>
|
||||
<i className="fas fa-plus" /> Create Agent
|
||||
</button>
|
||||
<label className="btn btn-secondary">
|
||||
<i className="fas fa-file-import" /> Import
|
||||
<input type="file" accept=".json" className="agents-import-input" onChange={handleImport} />
|
||||
</label>
|
||||
{agentHubURL && (
|
||||
<a className="btn btn-secondary" href={agentHubURL} target="_blank" rel="noopener noreferrer">
|
||||
<i className="fas fa-store" /> Agent Hub
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="agents-toolbar">
|
||||
<div className="agents-search">
|
||||
<i className="fas fa-search" />
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="Search agents..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<span style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)' }}>
|
||||
{filtered.length} of {agents.length} agent{agents.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon"><i className="fas fa-search" /></div>
|
||||
<h2 className="empty-state-title">No matching agents</h2>
|
||||
<p className="empty-state-text">No agents match "{search}"</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="table-container">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th style={{ textAlign: 'right' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.map(agent => {
|
||||
const name = agent.name || agent.id
|
||||
const isActive = agent.status === 'active'
|
||||
return (
|
||||
<tr key={name}>
|
||||
<td>
|
||||
<a className="agents-name" onClick={() => navigate(`/agents/${encodeURIComponent(name)}/chat`)}>
|
||||
{name}
|
||||
</a>
|
||||
</td>
|
||||
<td>{statusBadge(agent.status)}</td>
|
||||
<td>
|
||||
<div className="agents-action-group">
|
||||
<button
|
||||
className={`btn btn-sm ${isActive ? 'btn-warning' : 'btn-success'}`}
|
||||
onClick={() => handlePauseResume(agent)}
|
||||
title={isActive ? 'Pause' : 'Resume'}
|
||||
>
|
||||
<i className={`fas ${isActive ? 'fa-pause' : 'fa-play'}`} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => navigate(`/agents/${encodeURIComponent(name)}/edit`)}
|
||||
title="Edit"
|
||||
>
|
||||
<i className="fas fa-edit" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => navigate(`/agents/${encodeURIComponent(name)}/chat`)}
|
||||
title="Chat"
|
||||
>
|
||||
<i className="fas fa-comment" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => navigate(`/agents/${encodeURIComponent(name)}/status`)}
|
||||
title="Status & Observables"
|
||||
>
|
||||
<i className="fas fa-chart-bar" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => handleExport(name)}
|
||||
title="Export"
|
||||
>
|
||||
<i className="fas fa-download" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => handleDelete(name)}
|
||||
title="Delete"
|
||||
>
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
509
core/http/react-ui/src/pages/CollectionDetails.jsx
Normal file
509
core/http/react-ui/src/pages/CollectionDetails.jsx
Normal file
@@ -0,0 +1,509 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useParams, useOutletContext } from 'react-router-dom'
|
||||
import { agentCollectionsApi } from '../utils/api'
|
||||
|
||||
export default function CollectionDetails() {
|
||||
const { name } = useParams()
|
||||
const { addToast } = useOutletContext()
|
||||
const [activeTab, setActiveTab] = useState('entries')
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// Entries tab state
|
||||
const [entries, setEntries] = useState([])
|
||||
const [uploadFile, setUploadFile] = useState(null)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
|
||||
// Search tab state
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [searchMaxResults, setSearchMaxResults] = useState(10)
|
||||
const [searchResults, setSearchResults] = useState([])
|
||||
const [searching, setSearching] = useState(false)
|
||||
|
||||
// Entry content modal state
|
||||
const [viewEntry, setViewEntry] = useState(null)
|
||||
const [viewContent, setViewContent] = useState(null)
|
||||
const [viewLoading, setViewLoading] = useState(false)
|
||||
|
||||
// Sources tab state
|
||||
const [sources, setSources] = useState([])
|
||||
const [newSourceUrl, setNewSourceUrl] = useState('')
|
||||
const [newSourceInterval, setNewSourceInterval] = useState('')
|
||||
const [addingSource, setAddingSource] = useState(false)
|
||||
|
||||
const fetchEntries = useCallback(async () => {
|
||||
try {
|
||||
const data = await agentCollectionsApi.entries(name)
|
||||
setEntries(Array.isArray(data.entries) ? data.entries : [])
|
||||
} catch (err) {
|
||||
addToast(`Failed to load entries: ${err.message}`, 'error')
|
||||
}
|
||||
}, [name, addToast])
|
||||
|
||||
const fetchSources = useCallback(async () => {
|
||||
try {
|
||||
const data = await agentCollectionsApi.sources(name)
|
||||
setSources(Array.isArray(data.sources) ? data.sources : [])
|
||||
} catch (err) {
|
||||
addToast(`Failed to load sources: ${err.message}`, 'error')
|
||||
}
|
||||
}, [name, addToast])
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
setLoading(true)
|
||||
await Promise.allSettled([fetchEntries(), fetchSources()])
|
||||
setLoading(false)
|
||||
}
|
||||
load()
|
||||
}, [fetchEntries, fetchSources])
|
||||
|
||||
const handleViewContent = async (entry) => {
|
||||
setViewEntry(entry)
|
||||
setViewContent(null)
|
||||
setViewLoading(true)
|
||||
try {
|
||||
const data = await agentCollectionsApi.entryContent(name, entry)
|
||||
setViewContent(data)
|
||||
} catch (err) {
|
||||
addToast(`Failed to load entry content: ${err.message}`, 'error')
|
||||
setViewEntry(null)
|
||||
} finally {
|
||||
setViewLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteEntry = async (entry) => {
|
||||
if (!window.confirm('Are you sure you want to delete this entry?')) return
|
||||
try {
|
||||
await agentCollectionsApi.deleteEntry(name, entry)
|
||||
addToast('Entry deleted', 'success')
|
||||
fetchEntries()
|
||||
} catch (err) {
|
||||
addToast(`Failed to delete entry: ${err.message}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpload = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!uploadFile) return
|
||||
setUploading(true)
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', uploadFile)
|
||||
await agentCollectionsApi.upload(name, formData)
|
||||
addToast('File uploaded successfully', 'success')
|
||||
setUploadFile(null)
|
||||
fetchEntries()
|
||||
} catch (err) {
|
||||
addToast(`Upload failed: ${err.message}`, 'error')
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!searchQuery.trim()) return
|
||||
setSearching(true)
|
||||
try {
|
||||
const data = await agentCollectionsApi.search(name, searchQuery, searchMaxResults)
|
||||
setSearchResults(Array.isArray(data.results) ? data.results : [])
|
||||
} catch (err) {
|
||||
addToast(`Search failed: ${err.message}`, 'error')
|
||||
} finally {
|
||||
setSearching(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddSource = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!newSourceUrl.trim()) return
|
||||
setAddingSource(true)
|
||||
try {
|
||||
await agentCollectionsApi.addSource(name, newSourceUrl, newSourceInterval || undefined)
|
||||
addToast('Source added', 'success')
|
||||
setNewSourceUrl('')
|
||||
setNewSourceInterval('')
|
||||
fetchSources()
|
||||
} catch (err) {
|
||||
addToast(`Failed to add source: ${err.message}`, 'error')
|
||||
} finally {
|
||||
setAddingSource(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveSource = async (url) => {
|
||||
if (!window.confirm('Are you sure you want to remove this source?')) return
|
||||
try {
|
||||
await agentCollectionsApi.removeSource(name, url)
|
||||
addToast('Source removed', 'success')
|
||||
fetchSources()
|
||||
} catch (err) {
|
||||
addToast(`Failed to remove source: ${err.message}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<style>{`
|
||||
.collection-detail-upload-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.collection-detail-search-form {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.collection-detail-search-form .collection-detail-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
.collection-detail-search-form .collection-detail-field label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.collection-detail-result-card {
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
.collection-detail-result-score {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
.collection-detail-result-content {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-primary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.collection-detail-source-form {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.collection-detail-source-form .collection-detail-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
.collection-detail-source-form .collection-detail-field label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.collection-detail-entry-content {
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.collection-detail-empty {
|
||||
text-align: center;
|
||||
padding: var(--spacing-xl);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.collection-detail-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.collection-detail-modal {
|
||||
background: var(--color-bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.collection-detail-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.collection-detail-modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.collection-detail-modal-body {
|
||||
padding: var(--spacing-lg);
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
.collection-detail-modal-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.8125rem;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md);
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">{name}</h1>
|
||||
<p className="page-subtitle">Collection details and management</p>
|
||||
</div>
|
||||
|
||||
<div className="tabs">
|
||||
<button className={`tab ${activeTab === 'entries' ? 'tab-active' : ''}`} onClick={() => setActiveTab('entries')}>
|
||||
<i className="fas fa-list" /> Entries
|
||||
</button>
|
||||
<button className={`tab ${activeTab === 'search' ? 'tab-active' : ''}`} onClick={() => setActiveTab('search')}>
|
||||
<i className="fas fa-search" /> Search
|
||||
</button>
|
||||
<button className={`tab ${activeTab === 'sources' ? 'tab-active' : ''}`} onClick={() => setActiveTab('sources')}>
|
||||
<i className="fas fa-globe" /> Sources
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--spacing-xl)' }}>
|
||||
<i className="fas fa-spinner fa-spin" style={{ fontSize: '2rem', color: 'var(--color-primary)' }} />
|
||||
</div>
|
||||
) : activeTab === 'entries' ? (
|
||||
<>
|
||||
<form className="collection-detail-upload-form" onSubmit={handleUpload}>
|
||||
<input
|
||||
className="input"
|
||||
type="file"
|
||||
onChange={(e) => setUploadFile(e.target.files[0] || null)}
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
/>
|
||||
<button className="btn btn-primary" type="submit" disabled={!uploadFile || uploading}>
|
||||
{uploading ? <><i className="fas fa-spinner fa-spin" /> Uploading...</> : <><i className="fas fa-upload" /> Upload</>}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{entries.length === 0 ? (
|
||||
<div className="collection-detail-empty">
|
||||
<i className="fas fa-inbox" style={{ fontSize: '2rem', marginBottom: 'var(--spacing-sm)', display: 'block' }} />
|
||||
<p>No entries in this collection. Upload a file to get started.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="table-container">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Entry</th>
|
||||
<th style={{ textAlign: 'right' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((entry, index) => (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<div className="collection-detail-entry-content">
|
||||
{typeof entry === 'string' ? entry : JSON.stringify(entry)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--spacing-xs)' }}>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => handleViewContent(entry)} title="View Content">
|
||||
<i className="fas fa-eye" />
|
||||
</button>
|
||||
<button className="btn btn-danger btn-sm" onClick={() => handleDeleteEntry(entry)} title="Delete">
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : activeTab === 'search' ? (
|
||||
<>
|
||||
<form className="collection-detail-search-form" onSubmit={handleSearch}>
|
||||
<div className="collection-detail-field" style={{ flex: 2, minWidth: 250 }}>
|
||||
<label htmlFor="search-query">Query</label>
|
||||
<input
|
||||
id="search-query"
|
||||
className="input"
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Enter search query..."
|
||||
/>
|
||||
</div>
|
||||
<div className="collection-detail-field" style={{ flex: 0, minWidth: 100 }}>
|
||||
<label htmlFor="search-max">Max Results</label>
|
||||
<input
|
||||
id="search-max"
|
||||
className="input"
|
||||
type="number"
|
||||
min={1}
|
||||
max={100}
|
||||
value={searchMaxResults}
|
||||
onChange={(e) => setSearchMaxResults(parseInt(e.target.value, 10) || 10)}
|
||||
style={{ width: 100 }}
|
||||
/>
|
||||
</div>
|
||||
<button className="btn btn-primary" type="submit" disabled={!searchQuery.trim() || searching}>
|
||||
{searching ? <><i className="fas fa-spinner fa-spin" /> Searching...</> : <><i className="fas fa-search" /> Search</>}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{searchResults.length === 0 ? (
|
||||
<div className="collection-detail-empty">
|
||||
<i className="fas fa-search" style={{ fontSize: '2rem', marginBottom: 'var(--spacing-sm)', display: 'block' }} />
|
||||
<p>No results. Enter a query and click Search.</p>
|
||||
</div>
|
||||
) : (
|
||||
searchResults.map((result, index) => (
|
||||
<div className="collection-detail-result-card" key={index}>
|
||||
<div className="collection-detail-result-score">
|
||||
Similarity: {typeof result.similarity === 'number' ? result.similarity.toFixed(4) : (result.score != null ? Number(result.score).toFixed(4) : 'N/A')}
|
||||
</div>
|
||||
<div className="collection-detail-result-content">
|
||||
{result.content || result.text || (typeof result === 'string' ? result : JSON.stringify(result))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<form className="collection-detail-source-form" onSubmit={handleAddSource}>
|
||||
<div className="collection-detail-field">
|
||||
<label htmlFor="source-url">URL</label>
|
||||
<input
|
||||
id="source-url"
|
||||
className="input"
|
||||
type="text"
|
||||
value={newSourceUrl}
|
||||
onChange={(e) => setNewSourceUrl(e.target.value)}
|
||||
placeholder="https://example.com/data"
|
||||
/>
|
||||
</div>
|
||||
<div className="collection-detail-field" style={{ flex: 0, minWidth: 160 }}>
|
||||
<label htmlFor="source-interval">Update Interval</label>
|
||||
<input
|
||||
id="source-interval"
|
||||
className="input"
|
||||
type="text"
|
||||
value={newSourceInterval}
|
||||
onChange={(e) => setNewSourceInterval(e.target.value)}
|
||||
placeholder="e.g. 1h, 30m"
|
||||
/>
|
||||
</div>
|
||||
<button className="btn btn-primary" type="submit" disabled={!newSourceUrl.trim() || addingSource}>
|
||||
{addingSource ? <><i className="fas fa-spinner fa-spin" /> Adding...</> : <><i className="fas fa-plus" /> Add Source</>}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{sources.length === 0 ? (
|
||||
<div className="collection-detail-empty">
|
||||
<i className="fas fa-globe" style={{ fontSize: '2rem', marginBottom: 'var(--spacing-sm)', display: 'block' }} />
|
||||
<p>No external sources configured. Add a URL to start ingesting data.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="table-container">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>URL</th>
|
||||
<th>Interval</th>
|
||||
<th style={{ textAlign: 'right' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sources.map((source, index) => (
|
||||
<tr key={index}>
|
||||
<td style={{ fontSize: '0.8125rem', wordBreak: 'break-all' }}>
|
||||
{typeof source === 'string' ? source : (source.url || JSON.stringify(source))}
|
||||
</td>
|
||||
<td style={{ fontSize: '0.8125rem', color: 'var(--color-text-secondary)' }}>
|
||||
{(typeof source === 'object' && source.update_interval) ? source.update_interval : '-'}
|
||||
</td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => handleRemoveSource(typeof source === 'string' ? source : source.url)}
|
||||
title="Remove"
|
||||
>
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Entry content modal */}
|
||||
{viewEntry && (
|
||||
<div className="collection-detail-modal-overlay" onClick={() => setViewEntry(null)}>
|
||||
<div className="collection-detail-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="collection-detail-modal-header">
|
||||
<h3 title={viewEntry}><i className="fas fa-file-alt" style={{ marginRight: 'var(--spacing-xs)' }} />{viewEntry}</h3>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => setViewEntry(null)}>
|
||||
<i className="fas fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="collection-detail-modal-body">
|
||||
{viewLoading ? (
|
||||
<div style={{ textAlign: 'center', padding: 'var(--spacing-lg)' }}>
|
||||
<i className="fas fa-spinner fa-spin" style={{ fontSize: '1.5rem', color: 'var(--color-primary)' }} />
|
||||
</div>
|
||||
) : viewContent ? (
|
||||
<>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-md)', marginBottom: 'var(--spacing-md)' }}>
|
||||
<div style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)' }}>
|
||||
<i className="fas fa-puzzle-piece" style={{ marginRight: 4 }} />
|
||||
Chunks: <strong style={{ color: 'var(--color-text-primary)' }}>{viewContent.chunk_count ?? '-'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="collection-detail-modal-content">
|
||||
{viewContent.content || '(empty)'}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p style={{ color: 'var(--color-text-muted)' }}>No content available.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
152
core/http/react-ui/src/pages/Collections.jsx
Normal file
152
core/http/react-ui/src/pages/Collections.jsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom'
|
||||
import { agentCollectionsApi } from '../utils/api'
|
||||
|
||||
export default function Collections() {
|
||||
const { addToast } = useOutletContext()
|
||||
const navigate = useNavigate()
|
||||
const [collections, setCollections] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [newName, setNewName] = useState('')
|
||||
const [creating, setCreating] = useState(false)
|
||||
|
||||
const fetchCollections = useCallback(async () => {
|
||||
try {
|
||||
const data = await agentCollectionsApi.list()
|
||||
setCollections(Array.isArray(data.collections) ? data.collections : [])
|
||||
} catch (err) {
|
||||
addToast(`Failed to load collections: ${err.message}`, 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [addToast])
|
||||
|
||||
useEffect(() => {
|
||||
fetchCollections()
|
||||
}, [fetchCollections])
|
||||
|
||||
const handleCreate = async () => {
|
||||
const name = newName.trim()
|
||||
if (!name) return
|
||||
setCreating(true)
|
||||
try {
|
||||
await agentCollectionsApi.create(name)
|
||||
addToast(`Collection "${name}" created`, 'success')
|
||||
setNewName('')
|
||||
fetchCollections()
|
||||
} catch (err) {
|
||||
addToast(`Failed to create collection: ${err.message}`, 'error')
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (name) => {
|
||||
if (!window.confirm(`Delete collection "${name}"? This will remove all entries and cannot be undone.`)) return
|
||||
try {
|
||||
await agentCollectionsApi.reset(name)
|
||||
addToast(`Collection "${name}" deleted`, 'success')
|
||||
fetchCollections()
|
||||
} catch (err) {
|
||||
addToast(`Failed to delete collection: ${err.message}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = async (name) => {
|
||||
if (!window.confirm(`Reset collection "${name}"? This will remove all entries but keep the collection.`)) return
|
||||
try {
|
||||
await agentCollectionsApi.reset(name)
|
||||
addToast(`Collection "${name}" reset`, 'success')
|
||||
fetchCollections()
|
||||
} catch (err) {
|
||||
addToast(`Failed to reset collection: ${err.message}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<style>{`
|
||||
.collections-create-bar {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
.collections-create-bar .input {
|
||||
flex: 1;
|
||||
}
|
||||
.collections-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
.collections-card-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
word-break: break-word;
|
||||
}
|
||||
.collections-card-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">Knowledge Base</h1>
|
||||
<p className="page-subtitle">Manage document collections for agent RAG</p>
|
||||
</div>
|
||||
|
||||
<div className="collections-create-bar">
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="New collection name..."
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleCreate() }}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={handleCreate} disabled={creating || !newName.trim()}>
|
||||
{creating ? <><i className="fas fa-spinner fa-spin" /> Creating...</> : <><i className="fas fa-plus" /> Create</>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--spacing-xl)' }}>
|
||||
<i className="fas fa-spinner fa-spin" style={{ fontSize: '2rem', color: 'var(--color-text-muted)' }} />
|
||||
</div>
|
||||
) : collections.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon"><i className="fas fa-database" /></div>
|
||||
<h2 className="empty-state-title">No collections yet</h2>
|
||||
<p className="empty-state-text">Create a collection above to start building your knowledge base.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="collections-grid">
|
||||
{collections.map((collection) => {
|
||||
const name = typeof collection === 'string' ? collection : collection.name
|
||||
return (
|
||||
<div className="card" key={name} style={{ cursor: 'pointer' }} onClick={() => navigate(`/collections/${encodeURIComponent(name)}`)}>
|
||||
<div className="collections-card-name">
|
||||
<i className="fas fa-folder" style={{ marginRight: 'var(--spacing-xs)', color: 'var(--color-primary)' }} />
|
||||
{name}
|
||||
</div>
|
||||
<div className="collections-card-actions" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => navigate(`/collections/${encodeURIComponent(name)}`)} title="View details">
|
||||
<i className="fas fa-eye" /> Details
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => handleReset(name)} title="Reset collection">
|
||||
<i className="fas fa-rotate" /> Reset
|
||||
</button>
|
||||
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(name)} title="Delete collection">
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useOutletContext } from 'react-router-dom'
|
||||
import { settingsApi, resourcesApi } from '../utils/api'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
import SearchableModelSelect from '../components/SearchableModelSelect'
|
||||
import { formatBytes, percentColor } from '../utils/format'
|
||||
|
||||
function Toggle({ checked, onChange, disabled }) {
|
||||
@@ -59,6 +60,7 @@ const SECTIONS = [
|
||||
{ id: 'galleries', icon: 'fa-images', color: 'var(--color-accent)', label: 'Galleries' },
|
||||
{ id: 'apikeys', icon: 'fa-key', color: 'var(--color-error)', label: 'API Keys' },
|
||||
{ id: 'agents', icon: 'fa-tasks', color: 'var(--color-primary)', label: 'Agent Jobs' },
|
||||
{ id: 'agentpool', icon: 'fa-robot', color: 'var(--color-primary)', label: 'Agent Pool' },
|
||||
{ id: 'responses', icon: 'fa-database', color: 'var(--color-accent)', label: 'Responses' },
|
||||
]
|
||||
|
||||
@@ -450,6 +452,36 @@ export default function Settings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent Pool */}
|
||||
<div ref={el => sectionRefs.current.agentpool = el} style={{ marginBottom: 'var(--spacing-xl)' }}>
|
||||
<h3 style={{ fontSize: '1rem', fontWeight: 700, display: 'flex', alignItems: 'center', gap: 'var(--spacing-sm)', marginBottom: 'var(--spacing-md)' }}>
|
||||
<i className="fas fa-robot" style={{ color: 'var(--color-primary)' }} /> Agent Pool
|
||||
</h3>
|
||||
<div className="card">
|
||||
<SettingRow label="Enabled" description="Enable or disable the agent pool feature (requires restart)">
|
||||
<Toggle checked={settings.agent_pool_enabled ?? true} onChange={(v) => update('agent_pool_enabled', v)} />
|
||||
</SettingRow>
|
||||
<SettingRow label="Default Model" description="Default LLM model for agents">
|
||||
<SearchableModelSelect value={settings.agent_pool_default_model || ''} onChange={(v) => update('agent_pool_default_model', v)} capability="FLAG_CHAT" placeholder="e.g. gpt-4" />
|
||||
</SettingRow>
|
||||
<SettingRow label="Embedding Model" description="Model used for knowledge base embeddings">
|
||||
<SearchableModelSelect value={settings.agent_pool_embedding_model || ''} onChange={(v) => update('agent_pool_embedding_model', v)} placeholder="granite-embedding-107m-multilingual" />
|
||||
</SettingRow>
|
||||
<SettingRow label="Max Chunking Size" description="Maximum chunk size for knowledge base documents (default: 400)">
|
||||
<input className="input" type="number" style={{ width: 120 }} value={settings.agent_pool_max_chunking_size ?? 400} onChange={(e) => update('agent_pool_max_chunking_size', parseInt(e.target.value, 10) || 0)} min={0} />
|
||||
</SettingRow>
|
||||
<SettingRow label="Chunk Overlap" description="Overlap between chunks for knowledge base documents (default: 0)">
|
||||
<input className="input" type="number" style={{ width: 120 }} value={settings.agent_pool_chunk_overlap ?? 0} onChange={(e) => update('agent_pool_chunk_overlap', parseInt(e.target.value, 10) || 0)} min={0} />
|
||||
</SettingRow>
|
||||
<SettingRow label="Enable Logs" description="Enable agent logging (requires restart)">
|
||||
<Toggle checked={settings.agent_pool_enable_logs ?? false} onChange={(v) => update('agent_pool_enable_logs', v)} />
|
||||
</SettingRow>
|
||||
<SettingRow label="Collection DB Path" description="Database path for agent collections">
|
||||
<input className="input" style={{ width: 280 }} value={settings.agent_pool_collection_db_path || ''} onChange={(e) => update('agent_pool_collection_db_path', e.target.value)} placeholder="Leave empty for default" />
|
||||
</SettingRow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Open Responses */}
|
||||
<div ref={el => sectionRefs.current.responses = el} style={{ marginBottom: 'var(--spacing-xl)' }}>
|
||||
<h3 style={{ fontSize: '1rem', fontWeight: 700, display: 'flex', alignItems: 'center', gap: 'var(--spacing-sm)', marginBottom: 'var(--spacing-md)' }}>
|
||||
|
||||
629
core/http/react-ui/src/pages/SkillEdit.jsx
Normal file
629
core/http/react-ui/src/pages/SkillEdit.jsx
Normal file
@@ -0,0 +1,629 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useNavigate, useLocation, useOutletContext } from 'react-router-dom'
|
||||
import { skillsApi } from '../utils/api'
|
||||
|
||||
const RESOURCE_PREFIXES = ['scripts/', 'references/', 'assets/']
|
||||
function isValidResourcePath(path) {
|
||||
return RESOURCE_PREFIXES.some((p) => path.startsWith(p)) && !path.includes('..')
|
||||
}
|
||||
|
||||
function ResourceGroup({ title, icon, items, readOnly, pathPrefix, onView, onDelete, onUpload }) {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 'var(--spacing-lg)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--spacing-sm)' }}>
|
||||
<h3
|
||||
style={{ margin: 0, fontWeight: 600, fontSize: '0.95rem', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 'var(--spacing-xs)' }}
|
||||
onClick={() => setCollapsed((v) => !v)}
|
||||
>
|
||||
<i className={`fas fa-chevron-${collapsed ? 'right' : 'down'}`} style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)' }} />
|
||||
<i className={`fas fa-${icon}`} style={{ color: 'var(--color-primary)' }} /> {title}
|
||||
<span className="badge" style={{ marginLeft: 'var(--spacing-xs)' }}>{items.length}</span>
|
||||
</h3>
|
||||
{!readOnly && (
|
||||
<button className="btn btn-primary btn-sm" onClick={() => onUpload(pathPrefix)}>
|
||||
<i className="fas fa-upload" /> Upload
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
items.length === 0 ? (
|
||||
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.875rem', padding: 'var(--spacing-sm)' }}>
|
||||
No {title.toLowerCase()} yet.
|
||||
</p>
|
||||
) : (
|
||||
<div>
|
||||
{items.map((res) => (
|
||||
<div
|
||||
key={res.path}
|
||||
className="skilledit-resource-item"
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<span style={{ fontWeight: 500 }}>{res.name}</span>
|
||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: '0.8rem', marginLeft: 'var(--spacing-sm)' }}>
|
||||
{res.mime_type} · {(res.size || 0).toLocaleString()} B
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-xs)' }}>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => onView(res)} title="View/Edit">
|
||||
<i className="fas fa-edit" /> View/Edit
|
||||
</button>
|
||||
{!readOnly && (
|
||||
<button className="btn btn-danger btn-sm" onClick={() => onDelete(res.path)} title="Delete">
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ResourcesSection({ skillName, addToast }) {
|
||||
const [data, setData] = useState({ scripts: [], references: [], assets: [], readOnly: false })
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [editor, setEditor] = useState({ open: false, path: '', name: '', content: '', readable: true, saving: false })
|
||||
const [upload, setUpload] = useState({ open: false, pathPrefix: 'assets/', file: null, pathInput: '', uploading: false })
|
||||
const [deletePath, setDeletePath] = useState(null)
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await skillsApi.listResources(skillName)
|
||||
setData({
|
||||
scripts: res.scripts || [],
|
||||
references: res.references || [],
|
||||
assets: res.assets || [],
|
||||
readOnly: res.readOnly === true,
|
||||
})
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Failed to load resources', 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [skillName])
|
||||
|
||||
const handleView = async (res) => {
|
||||
setEditor({ open: true, path: res.path, name: res.name, content: '', readable: res.readable !== false, saving: false })
|
||||
if (res.readable !== false) {
|
||||
try {
|
||||
const json = await skillsApi.getResource(skillName, res.path, { json: true })
|
||||
const content = json.encoding === 'base64' && json.content ? atob(json.content) : (json.content || '')
|
||||
setEditor((e) => ({ ...e, content }))
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Failed to load file', 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditorSave = async () => {
|
||||
setEditor((e) => ({ ...e, saving: true }))
|
||||
try {
|
||||
await skillsApi.updateResource(skillName, editor.path, editor.content)
|
||||
addToast('Resource updated', 'success')
|
||||
setEditor((e) => ({ ...e, open: false }))
|
||||
load()
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Update failed', 'error')
|
||||
} finally {
|
||||
setEditor((e) => ({ ...e, saving: false }))
|
||||
}
|
||||
}
|
||||
|
||||
const handleUploadOpen = (pathPrefix) => {
|
||||
setUpload({ open: true, pathPrefix, file: null, pathInput: '', uploading: false })
|
||||
}
|
||||
|
||||
const handleUploadSubmit = async () => {
|
||||
const path = upload.pathInput.trim() || (upload.file ? upload.pathPrefix + upload.file.name : '')
|
||||
if (!path || !upload.file) {
|
||||
addToast('Select a file and ensure path is set', 'error')
|
||||
return
|
||||
}
|
||||
if (!isValidResourcePath(path)) {
|
||||
addToast('Path must start with scripts/, references/, or assets/', 'error')
|
||||
return
|
||||
}
|
||||
setUpload((u) => ({ ...u, uploading: true }))
|
||||
try {
|
||||
await skillsApi.createResource(skillName, path, upload.file)
|
||||
addToast('Resource added', 'success')
|
||||
setUpload((u) => ({ ...u, open: false }))
|
||||
load()
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Upload failed', 'error')
|
||||
} finally {
|
||||
setUpload((u) => ({ ...u, uploading: false }))
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!deletePath) return
|
||||
try {
|
||||
await skillsApi.deleteResource(skillName, deletePath)
|
||||
addToast('Resource deleted', 'success')
|
||||
setDeletePath(null)
|
||||
load()
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Delete failed', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 style={{ fontWeight: 600, marginBottom: 'var(--spacing-sm)' }}>
|
||||
<i className="fas fa-folder" style={{ color: 'var(--color-primary)', marginRight: 'var(--spacing-xs)' }} /> Resources
|
||||
</h3>
|
||||
<p style={{ color: 'var(--color-text-secondary)', fontSize: '0.875rem', marginBottom: 'var(--spacing-md)' }}>
|
||||
Scripts, references, and assets for this skill. Paths must start with scripts/, references/, or assets/.
|
||||
</p>
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--spacing-md)' }}>
|
||||
<i className="fas fa-spinner fa-spin" style={{ fontSize: '1.5rem', color: 'var(--color-text-muted)' }} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<ResourceGroup title="Scripts" icon="code" pathPrefix="scripts/" items={data.scripts} readOnly={data.readOnly} onView={handleView} onDelete={setDeletePath} onUpload={handleUploadOpen} />
|
||||
<ResourceGroup title="References" icon="book" pathPrefix="references/" items={data.references} readOnly={data.readOnly} onView={handleView} onDelete={setDeletePath} onUpload={handleUploadOpen} />
|
||||
<ResourceGroup title="Assets" icon="image" pathPrefix="assets/" items={data.assets} readOnly={data.readOnly} onView={handleView} onDelete={setDeletePath} onUpload={handleUploadOpen} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{editor.open && (
|
||||
<div className="skilledit-modal-overlay" onClick={() => !editor.saving && setEditor((e) => ({ ...e, open: false }))}>
|
||||
<div className="card skilledit-modal-card" style={{ maxWidth: '700px' }} onClick={(e) => e.stopPropagation()}>
|
||||
<h3 style={{ fontWeight: 600, marginBottom: 'var(--spacing-md)' }}>
|
||||
<i className="fas fa-edit" style={{ color: 'var(--color-primary)', marginRight: 'var(--spacing-xs)' }} /> Edit {editor.name}
|
||||
</h3>
|
||||
{editor.readable ? (
|
||||
<>
|
||||
<textarea
|
||||
className="input"
|
||||
value={editor.content}
|
||||
onChange={(e) => setEditor((x) => ({ ...x, content: e.target.value }))}
|
||||
rows={14}
|
||||
style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.875rem', marginBottom: 'var(--spacing-md)', width: '100%' }}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)', justifyContent: 'flex-end' }}>
|
||||
<button className="btn btn-secondary" onClick={() => setEditor((e) => ({ ...e, open: false }))}>Cancel</button>
|
||||
<button className="btn btn-primary" disabled={editor.saving} onClick={handleEditorSave}>
|
||||
{editor.saving ? <><i className="fas fa-spinner fa-spin" /> Saving...</> : <><i className="fas fa-save" /> Save</>}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p style={{ color: 'var(--color-text-secondary)' }}>Binary file. Download via API or export skill.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{upload.open && (
|
||||
<div className="skilledit-modal-overlay" onClick={() => !upload.uploading && setUpload((u) => ({ ...u, open: false }))}>
|
||||
<div className="card skilledit-modal-card" style={{ maxWidth: '400px' }} onClick={(e) => e.stopPropagation()}>
|
||||
<h3 style={{ fontWeight: 600, marginBottom: 'var(--spacing-md)' }}>
|
||||
<i className="fas fa-upload" style={{ color: 'var(--color-primary)', marginRight: 'var(--spacing-xs)' }} /> Upload to {upload.pathPrefix}
|
||||
</h3>
|
||||
<div className="form-group">
|
||||
<label className="form-label">File</label>
|
||||
<input
|
||||
type="file"
|
||||
className="input"
|
||||
onChange={(e) => setUpload((u) => ({ ...u, file: e.target.files?.[0] || null }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Path (default: {upload.pathPrefix} + filename)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder={`${upload.pathPrefix}filename`}
|
||||
value={upload.pathInput}
|
||||
onChange={(e) => setUpload((u) => ({ ...u, pathInput: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)', justifyContent: 'flex-end', marginTop: 'var(--spacing-md)' }}>
|
||||
<button className="btn btn-secondary" onClick={() => setUpload((u) => ({ ...u, open: false }))}>Cancel</button>
|
||||
<button className="btn btn-primary" disabled={upload.uploading || !upload.file} onClick={handleUploadSubmit}>
|
||||
{upload.uploading ? <><i className="fas fa-spinner fa-spin" /> Uploading...</> : <><i className="fas fa-upload" /> Upload</>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{deletePath && (
|
||||
<div className="skilledit-modal-overlay" onClick={() => setDeletePath(null)}>
|
||||
<div className="card skilledit-modal-card" style={{ maxWidth: '360px' }} onClick={(e) => e.stopPropagation()}>
|
||||
<p style={{ marginBottom: 'var(--spacing-md)' }}>Delete resource <strong>{deletePath}</strong>?</p>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)', justifyContent: 'flex-end' }}>
|
||||
<button className="btn btn-secondary" onClick={() => setDeletePath(null)}>Cancel</button>
|
||||
<button className="btn btn-danger" onClick={handleDeleteConfirm}>
|
||||
<i className="fas fa-trash" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SkillEdit() {
|
||||
const { name: nameParam } = useParams()
|
||||
const location = useLocation()
|
||||
const isNew = location.pathname.endsWith('/new')
|
||||
const name = nameParam ? decodeURIComponent(nameParam) : undefined
|
||||
const navigate = useNavigate()
|
||||
const { addToast } = useOutletContext()
|
||||
const [loading, setLoading] = useState(!isNew)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [activeSection, setActiveSection] = useState('basic')
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
content: '',
|
||||
license: '',
|
||||
compatibility: '',
|
||||
metadata: {},
|
||||
allowedTools: '',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isNew) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
if (name) {
|
||||
skillsApi.get(name)
|
||||
.then((data) => {
|
||||
setForm({
|
||||
name: data.name || '',
|
||||
description: data.description || '',
|
||||
content: data.content || '',
|
||||
license: data.license || '',
|
||||
compatibility: data.compatibility || '',
|
||||
metadata: data.metadata || {},
|
||||
allowedTools: data['allowed-tools'] || '',
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
addToast(err.message || 'Failed to load skill', 'error')
|
||||
navigate('/skills')
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
}, [isNew, name, navigate, addToast])
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!form.name.trim()) {
|
||||
addToast('Skill name is required', 'warning')
|
||||
return
|
||||
}
|
||||
if (!form.description.trim()) {
|
||||
addToast('Skill description is required', 'warning')
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
try {
|
||||
const payload = {
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
content: form.content,
|
||||
license: form.license || undefined,
|
||||
compatibility: form.compatibility || undefined,
|
||||
metadata: Object.keys(form.metadata).length ? form.metadata : undefined,
|
||||
'allowed-tools': form.allowedTools || undefined,
|
||||
}
|
||||
if (isNew) {
|
||||
await skillsApi.create(payload)
|
||||
addToast('Skill created', 'success')
|
||||
} else {
|
||||
await skillsApi.update(name, { ...payload, name: undefined })
|
||||
addToast('Skill updated', 'success')
|
||||
}
|
||||
navigate('/skills')
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Save failed', 'error')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="page" style={{ display: 'flex', justifyContent: 'center', padding: 'var(--spacing-xl)' }}>
|
||||
<i className="fas fa-spinner fa-spin" style={{ fontSize: '2rem', color: 'var(--color-primary)' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ id: 'basic', label: 'Basic information', icon: 'fa-info-circle' },
|
||||
{ id: 'content', label: 'Content', icon: 'fa-file-alt' },
|
||||
...(!isNew && name ? [{ id: 'resources', label: 'Resources', icon: 'fa-folder' }] : []),
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="page" style={{ maxWidth: 960 }}>
|
||||
<style>{`
|
||||
.skilledit-back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.skilledit-back-link:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.skilledit-layout {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
.skilledit-sidebar {
|
||||
flex-shrink: 0;
|
||||
width: 200px;
|
||||
}
|
||||
.skilledit-sidebar-nav {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.skilledit-sidebar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
border-left: 3px solid transparent;
|
||||
transition: all var(--duration-fast) var(--ease-default);
|
||||
}
|
||||
.skilledit-sidebar-item:hover {
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
.skilledit-sidebar-item.active {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
border-left-color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
.skilledit-form-area {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.skilledit-section-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
.skilledit-field {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
.skilledit-field label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.skilledit-field .required {
|
||||
color: var(--color-error);
|
||||
}
|
||||
.skilledit-field .help-text {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
.skilledit-form-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--spacing-lg);
|
||||
padding-top: var(--spacing-md);
|
||||
border-top: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
.skilledit-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--color-bg-overlay);
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
.skilledit-modal-card {
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
.skilledit-resource-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
background: var(--color-bg-tertiary);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.skilledit-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
.skilledit-sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
.skilledit-sidebar-nav {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
overflow-x: auto;
|
||||
}
|
||||
.skilledit-sidebar-item {
|
||||
border-left: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.skilledit-sidebar-item.active {
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<a className="skilledit-back-link" onClick={() => navigate('/skills')}>
|
||||
<i className="fas fa-arrow-left" /> Back to skills
|
||||
</a>
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">
|
||||
<i className="fas fa-book" style={{ marginRight: 'var(--spacing-xs)' }} /> {isNew ? 'New skill' : `Edit: ${name}`}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginTop: 'var(--spacing-md)' }}>
|
||||
<div className="skilledit-layout">
|
||||
<div className="skilledit-sidebar">
|
||||
<ul className="skilledit-sidebar-nav">
|
||||
{sections.map((s) => (
|
||||
<li
|
||||
key={s.id}
|
||||
className={`skilledit-sidebar-item ${activeSection === s.id ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection(s.id)}
|
||||
>
|
||||
<i className={`fas ${s.icon}`} /> {s.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="skilledit-form-area">
|
||||
<form onSubmit={handleSubmit} noValidate>
|
||||
<div style={{ display: activeSection === 'basic' ? 'block' : 'none' }}>
|
||||
<h3 className="skilledit-section-title">Basic information</h3>
|
||||
<div className="skilledit-field">
|
||||
<label htmlFor="skill-name">Name (lowercase, hyphens only) <span className="required">*</span></label>
|
||||
<input
|
||||
id="skill-name"
|
||||
type="text"
|
||||
className="input"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
||||
required
|
||||
disabled={!isNew}
|
||||
placeholder="my-skill"
|
||||
/>
|
||||
{!isNew && <p className="help-text">Name cannot be changed after creation.</p>}
|
||||
</div>
|
||||
<div className="skilledit-field">
|
||||
<label htmlFor="skill-desc">Description (required, max 1024 chars) <span className="required">*</span></label>
|
||||
<textarea
|
||||
id="skill-desc"
|
||||
className="input"
|
||||
value={form.description}
|
||||
onChange={(e) => setForm((f) => ({ ...f, description: e.target.value }))}
|
||||
required
|
||||
maxLength={1024}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="skilledit-field">
|
||||
<label htmlFor="skill-license">License (optional)</label>
|
||||
<input
|
||||
id="skill-license"
|
||||
type="text"
|
||||
className="input"
|
||||
value={form.license}
|
||||
onChange={(e) => setForm((f) => ({ ...f, license: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="skilledit-field">
|
||||
<label htmlFor="skill-compat">Compatibility (optional, max 500 chars)</label>
|
||||
<input
|
||||
id="skill-compat"
|
||||
type="text"
|
||||
className="input"
|
||||
value={form.compatibility}
|
||||
onChange={(e) => setForm((f) => ({ ...f, compatibility: e.target.value }))}
|
||||
maxLength={500}
|
||||
/>
|
||||
</div>
|
||||
<div className="skilledit-field">
|
||||
<label htmlFor="skill-allowed-tools">Allowed tools (optional)</label>
|
||||
<input
|
||||
id="skill-allowed-tools"
|
||||
type="text"
|
||||
className="input"
|
||||
value={form.allowedTools}
|
||||
onChange={(e) => setForm((f) => ({ ...f, allowedTools: e.target.value }))}
|
||||
placeholder="tool1, tool2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'content' ? 'block' : 'none' }}>
|
||||
<h3 className="skilledit-section-title">Content</h3>
|
||||
<div className="skilledit-field">
|
||||
<label htmlFor="skill-content">Skill content (markdown)</label>
|
||||
<textarea
|
||||
id="skill-content"
|
||||
className="input"
|
||||
value={form.content}
|
||||
onChange={(e) => setForm((f) => ({ ...f, content: e.target.value }))}
|
||||
rows={14}
|
||||
style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.875rem' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeSection === 'resources' && (
|
||||
<div>
|
||||
{isNew || !name ? (
|
||||
<div>
|
||||
<h3 className="skilledit-section-title">Resources</h3>
|
||||
<p style={{ color: 'var(--color-text-secondary)' }}>
|
||||
Save the skill first to add scripts, references, and assets. After creating the skill, use this tab to upload files and manage resources.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<ResourcesSection skillName={name} addToast={addToast} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="skilledit-form-actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={() => navigate('/skills')}>
|
||||
<i className="fas fa-times" /> Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={saving}>
|
||||
<i className="fas fa-save" /> {saving ? 'Saving...' : (isNew ? 'Create skill' : 'Save changes')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
452
core/http/react-ui/src/pages/Skills.jsx
Normal file
452
core/http/react-ui/src/pages/Skills.jsx
Normal file
@@ -0,0 +1,452 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom'
|
||||
import { skillsApi } from '../utils/api'
|
||||
|
||||
export default function Skills() {
|
||||
const { addToast } = useOutletContext()
|
||||
const navigate = useNavigate()
|
||||
const [skills, setSkills] = useState([])
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [importing, setImporting] = useState(false)
|
||||
const [unavailable, setUnavailable] = useState(false)
|
||||
const [showGitRepos, setShowGitRepos] = useState(false)
|
||||
const [gitRepos, setGitRepos] = useState([])
|
||||
const [gitRepoUrl, setGitRepoUrl] = useState('')
|
||||
const [gitReposLoading, setGitReposLoading] = useState(false)
|
||||
const [gitReposAction, setGitReposAction] = useState(null)
|
||||
|
||||
const fetchSkills = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setUnavailable(false)
|
||||
const timeoutMs = 15000
|
||||
const withTimeout = (p) =>
|
||||
Promise.race([
|
||||
p,
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Request timed out')), timeoutMs)
|
||||
),
|
||||
])
|
||||
try {
|
||||
if (searchQuery.trim()) {
|
||||
const data = await withTimeout(skillsApi.search(searchQuery.trim()))
|
||||
setSkills(Array.isArray(data) ? data : [])
|
||||
} else {
|
||||
const data = await withTimeout(skillsApi.list())
|
||||
setSkills(Array.isArray(data) ? data : [])
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.message?.includes('503') || err.message?.includes('skills')) {
|
||||
setUnavailable(true)
|
||||
setSkills([])
|
||||
} else {
|
||||
addToast(err.message || 'Failed to load skills', 'error')
|
||||
setSkills([])
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [searchQuery, addToast])
|
||||
|
||||
useEffect(() => {
|
||||
fetchSkills()
|
||||
}, [fetchSkills])
|
||||
|
||||
const deleteSkill = async (name) => {
|
||||
if (!window.confirm(`Delete skill "${name}"? This action cannot be undone.`)) return
|
||||
try {
|
||||
await skillsApi.delete(name)
|
||||
addToast(`Skill "${name}" deleted`, 'success')
|
||||
fetchSkills()
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Failed to delete skill', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const exportSkill = async (name) => {
|
||||
try {
|
||||
const url = skillsApi.exportUrl(name)
|
||||
const res = await fetch(url, { credentials: 'same-origin' })
|
||||
if (!res.ok) throw new Error(res.statusText || 'Export failed')
|
||||
const blob = await res.blob()
|
||||
const a = document.createElement('a')
|
||||
a.href = URL.createObjectURL(blob)
|
||||
a.download = `${name.replace(/\//g, '-')}.tar.gz`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(a.href)
|
||||
addToast(`Skill "${name}" exported`, 'success')
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Export failed', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = async (e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
setImporting(true)
|
||||
try {
|
||||
await skillsApi.import(file)
|
||||
addToast(`Skill imported from "${file.name}"`, 'success')
|
||||
fetchSkills()
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Import failed', 'error')
|
||||
} finally {
|
||||
setImporting(false)
|
||||
e.target.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const loadGitRepos = async () => {
|
||||
setGitReposLoading(true)
|
||||
try {
|
||||
const list = await skillsApi.listGitRepos()
|
||||
setGitRepos(Array.isArray(list) ? list : [])
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Failed to load Git repos', 'error')
|
||||
setGitRepos([])
|
||||
} finally {
|
||||
setGitReposLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (showGitRepos) loadGitRepos()
|
||||
}, [showGitRepos])
|
||||
|
||||
const addGitRepo = async (e) => {
|
||||
e.preventDefault()
|
||||
const url = gitRepoUrl.trim()
|
||||
if (!url) return
|
||||
setGitReposAction('add')
|
||||
try {
|
||||
await skillsApi.addGitRepo(url)
|
||||
setGitRepoUrl('')
|
||||
await loadGitRepos()
|
||||
fetchSkills()
|
||||
addToast('Git repo added and syncing', 'success')
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Failed to add repo', 'error')
|
||||
} finally {
|
||||
setGitReposAction(null)
|
||||
}
|
||||
}
|
||||
|
||||
const syncGitRepo = async (id) => {
|
||||
setGitReposAction(id)
|
||||
try {
|
||||
await skillsApi.syncGitRepo(id)
|
||||
await loadGitRepos()
|
||||
fetchSkills()
|
||||
addToast('Repo synced', 'success')
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Sync failed', 'error')
|
||||
} finally {
|
||||
setGitReposAction(null)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleGitRepo = async (id) => {
|
||||
try {
|
||||
await skillsApi.toggleGitRepo(id)
|
||||
await loadGitRepos()
|
||||
fetchSkills()
|
||||
addToast('Repo toggled', 'success')
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Toggle failed', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const deleteGitRepo = async (id) => {
|
||||
if (!window.confirm('Remove this Git repository? Skills from it will no longer be available.')) return
|
||||
try {
|
||||
await skillsApi.deleteGitRepo(id)
|
||||
await loadGitRepos()
|
||||
fetchSkills()
|
||||
addToast('Repo removed', 'success')
|
||||
} catch (err) {
|
||||
addToast(err.message || 'Remove failed', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
if (unavailable) {
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">Skills</h1>
|
||||
<p className="page-subtitle">Skills service is not available or the index is rebuilding. Try again in a moment.</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--spacing-xl)' }}>
|
||||
<button className="btn btn-primary" onClick={() => { setUnavailable(false); fetchSkills() }}>
|
||||
<i className="fas fa-redo" /> Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<style>{`
|
||||
.skills-header-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.skills-import-input {
|
||||
display: none;
|
||||
}
|
||||
.skills-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
.skills-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
.skills-card-name {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
.skills-card-desc {
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.skills-card-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.skills-git-section {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding: var(--spacing-md);
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
.skills-git-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 var(--spacing-sm) 0;
|
||||
}
|
||||
.skills-git-desc {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
.skills-git-form {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
.skills-git-form .input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
.skills-git-repo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
background: var(--color-bg-tertiary);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.skills-git-repo-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
.skills-git-repo-url {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
margin-left: var(--spacing-sm);
|
||||
}
|
||||
.skills-git-repo-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<h1 className="page-title">Skills</h1>
|
||||
<p className="page-subtitle">Manage agent skills (reusable instructions and resources)</p>
|
||||
</div>
|
||||
<div className="skills-header-actions">
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="Search skills..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
style={{ width: '200px' }}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={() => navigate('/skills/new')}>
|
||||
<i className="fas fa-plus" /> New skill
|
||||
</button>
|
||||
<label className="btn btn-secondary" style={{ cursor: 'pointer' }}>
|
||||
<i className="fas fa-file-import" /> {importing ? 'Importing...' : 'Import'}
|
||||
<input
|
||||
type="file"
|
||||
accept=".tar.gz"
|
||||
className="skills-import-input"
|
||||
onChange={handleImport}
|
||||
disabled={importing}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className={`btn ${showGitRepos ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => setShowGitRepos((v) => !v)}
|
||||
>
|
||||
<i className="fas fa-code-branch" /> Git Repos
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showGitRepos && (
|
||||
<div className="skills-git-section">
|
||||
<h2 className="skills-git-title">
|
||||
<i className="fas fa-code-branch" style={{ marginRight: 'var(--spacing-xs)', color: 'var(--color-primary)' }} /> Git repositories
|
||||
</h2>
|
||||
<p className="skills-git-desc">
|
||||
Add Git repositories to pull skills from. Skills will appear in the list after sync.
|
||||
</p>
|
||||
<form onSubmit={addGitRepo} className="skills-git-form">
|
||||
<input
|
||||
type="url"
|
||||
className="input"
|
||||
placeholder="https://github.com/user/repo or git@github.com:user/repo.git"
|
||||
value={gitRepoUrl}
|
||||
onChange={(e) => setGitRepoUrl(e.target.value)}
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary" disabled={gitReposAction === 'add'}>
|
||||
{gitReposAction === 'add' ? <><i className="fas fa-spinner fa-spin" /> Adding...</> : 'Add repo'}
|
||||
</button>
|
||||
</form>
|
||||
{gitReposLoading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--spacing-md)' }}>
|
||||
<i className="fas fa-spinner fa-spin" style={{ fontSize: '1.5rem', color: 'var(--color-text-muted)' }} />
|
||||
</div>
|
||||
) : gitRepos.length === 0 ? (
|
||||
<p style={{ color: 'var(--color-text-secondary)', fontSize: '0.875rem' }}>No Git repos configured. Add one above.</p>
|
||||
) : (
|
||||
<div>
|
||||
{gitRepos.map((r) => (
|
||||
<div key={r.id} className="skills-git-repo-item">
|
||||
<div>
|
||||
<span className="skills-git-repo-name">{r.name || r.url}</span>
|
||||
<span className="skills-git-repo-url">{r.url}</span>
|
||||
{!r.enabled && <span className="badge" style={{ marginLeft: 'var(--spacing-sm)' }}>Disabled</span>}
|
||||
</div>
|
||||
<div className="skills-git-repo-actions">
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => syncGitRepo(r.id)}
|
||||
disabled={gitReposAction === r.id}
|
||||
title="Sync"
|
||||
>
|
||||
{gitReposAction === r.id ? <i className="fas fa-spinner fa-spin" /> : <><i className="fas fa-sync-alt" /> Sync</>}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => toggleGitRepo(r.id)}
|
||||
title={r.enabled ? 'Disable' : 'Enable'}
|
||||
>
|
||||
<i className={`fas fa-toggle-${r.enabled ? 'on' : 'off'}`} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => deleteGitRepo(r.id)}
|
||||
title="Remove repo"
|
||||
>
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--spacing-xl)' }}>
|
||||
<i className="fas fa-spinner fa-spin" style={{ fontSize: '2rem', color: 'var(--color-primary)' }} />
|
||||
</div>
|
||||
) : skills.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon"><i className="fas fa-book" /></div>
|
||||
<h2 className="empty-state-title">No skills found</h2>
|
||||
<p className="empty-state-text">Create a skill or import one to get started.</p>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)', justifyContent: 'center' }}>
|
||||
<button className="btn btn-primary" onClick={() => navigate('/skills/new')}>
|
||||
<i className="fas fa-plus" /> Create skill
|
||||
</button>
|
||||
<label className="btn btn-secondary" style={{ cursor: 'pointer' }}>
|
||||
<i className="fas fa-file-import" /> Import
|
||||
<input
|
||||
type="file"
|
||||
accept=".tar.gz"
|
||||
className="skills-import-input"
|
||||
onChange={handleImport}
|
||||
disabled={importing}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="skills-grid">
|
||||
{skills.map((s) => (
|
||||
<div key={s.name} className="card">
|
||||
<div className="skills-card-header">
|
||||
<h3 className="skills-card-name">{s.name}</h3>
|
||||
{s.readOnly && <span className="badge">Read-only</span>}
|
||||
</div>
|
||||
<p className="skills-card-desc">
|
||||
{s.description || 'No description'}
|
||||
</p>
|
||||
<div className="skills-card-actions">
|
||||
{!s.readOnly && (
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => navigate(`/skills/edit/${encodeURIComponent(s.name)}`)}
|
||||
title="Edit skill"
|
||||
>
|
||||
<i className="fas fa-edit" /> Edit
|
||||
</button>
|
||||
)}
|
||||
{!s.readOnly && (
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => deleteSkill(s.name)}
|
||||
title="Delete skill"
|
||||
>
|
||||
<i className="fas fa-trash" /> Delete
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => exportSkill(s.name)}
|
||||
title="Export as .tar.gz"
|
||||
>
|
||||
<i className="fas fa-download" /> Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,14 @@ import Backends from './pages/Backends'
|
||||
import Settings from './pages/Settings'
|
||||
import Traces from './pages/Traces'
|
||||
import P2P from './pages/P2P'
|
||||
import Agents from './pages/Agents'
|
||||
import AgentCreate from './pages/AgentCreate'
|
||||
import AgentChat from './pages/AgentChat'
|
||||
import AgentStatus from './pages/AgentStatus'
|
||||
import Collections from './pages/Collections'
|
||||
import CollectionDetails from './pages/CollectionDetails'
|
||||
import Skills from './pages/Skills'
|
||||
import SkillEdit from './pages/SkillEdit'
|
||||
import AgentJobs from './pages/AgentJobs'
|
||||
import AgentTaskDetails from './pages/AgentTaskDetails'
|
||||
import AgentJobDetails from './pages/AgentJobDetails'
|
||||
@@ -53,6 +61,16 @@ export const router = createBrowserRouter([
|
||||
{ path: 'settings', element: <Settings /> },
|
||||
{ path: 'traces', element: <Traces /> },
|
||||
{ path: 'p2p', element: <P2P /> },
|
||||
{ path: 'agents', element: <Agents /> },
|
||||
{ path: 'agents/new', element: <AgentCreate /> },
|
||||
{ path: 'agents/:name/edit', element: <AgentCreate /> },
|
||||
{ path: 'agents/:name/chat', element: <AgentChat /> },
|
||||
{ path: 'agents/:name/status', element: <AgentStatus /> },
|
||||
{ path: 'collections', element: <Collections /> },
|
||||
{ path: 'collections/:name', element: <CollectionDetails /> },
|
||||
{ path: 'skills', element: <Skills /> },
|
||||
{ path: 'skills/new', element: <SkillEdit /> },
|
||||
{ path: 'skills/edit/:name', element: <SkillEdit /> },
|
||||
{ path: 'agent-jobs', element: <AgentJobs /> },
|
||||
{ path: 'agent-jobs/tasks/new', element: <AgentTaskDetails /> },
|
||||
{ path: 'agent-jobs/tasks/:id', element: <AgentTaskDetails /> },
|
||||
|
||||
54
core/http/react-ui/src/utils/api.js
vendored
54
core/http/react-ui/src/utils/api.js
vendored
@@ -239,6 +239,60 @@ export const systemApi = {
|
||||
info: () => fetchJSON(API_CONFIG.endpoints.system),
|
||||
}
|
||||
|
||||
export const agentsApi = {
|
||||
list: () => fetchJSON('/api/agents'),
|
||||
create: (config) => postJSON('/api/agents', config),
|
||||
get: (name) => fetchJSON(`/api/agents/${encodeURIComponent(name)}`),
|
||||
getConfig: (name) => fetchJSON(`/api/agents/${encodeURIComponent(name)}/config`),
|
||||
update: (name, config) => fetchJSON(`/api/agents/${encodeURIComponent(name)}`, { method: 'PUT', body: JSON.stringify(config), headers: { 'Content-Type': 'application/json' } }),
|
||||
delete: (name) => fetchJSON(`/api/agents/${encodeURIComponent(name)}`, { method: 'DELETE' }),
|
||||
pause: (name) => fetchJSON(`/api/agents/${encodeURIComponent(name)}/pause`, { method: 'PUT' }),
|
||||
resume: (name) => fetchJSON(`/api/agents/${encodeURIComponent(name)}/resume`, { method: 'PUT' }),
|
||||
status: (name) => fetchJSON(`/api/agents/${encodeURIComponent(name)}/status`),
|
||||
observables: (name) => fetchJSON(`/api/agents/${encodeURIComponent(name)}/observables`),
|
||||
clearObservables: (name) => fetchJSON(`/api/agents/${encodeURIComponent(name)}/observables`, { method: 'DELETE' }),
|
||||
chat: (name, message) => postJSON(`/api/agents/${encodeURIComponent(name)}/chat`, { message }),
|
||||
export: (name) => fetchJSON(`/api/agents/${encodeURIComponent(name)}/export`),
|
||||
import: (formData) => fetch('/api/agents/import', { method: 'POST', body: formData }).then(handleResponse),
|
||||
configMeta: () => fetchJSON('/api/agents/config/metadata'),
|
||||
}
|
||||
|
||||
export const agentCollectionsApi = {
|
||||
list: () => fetchJSON('/api/agents/collections'),
|
||||
create: (name) => postJSON('/api/agents/collections', { name }),
|
||||
upload: (name, formData) => fetch(`/api/agents/collections/${encodeURIComponent(name)}/upload`, { method: 'POST', body: formData }).then(handleResponse),
|
||||
entries: (name) => fetchJSON(`/api/agents/collections/${encodeURIComponent(name)}/entries`),
|
||||
entryContent: (name, entry) => fetchJSON(`/api/agents/collections/${encodeURIComponent(name)}/entries/${encodeURIComponent(entry)}`),
|
||||
search: (name, query, maxResults) => postJSON(`/api/agents/collections/${encodeURIComponent(name)}/search`, { query, max_results: maxResults }),
|
||||
reset: (name) => postJSON(`/api/agents/collections/${encodeURIComponent(name)}/reset`),
|
||||
deleteEntry: (name, entry) => fetchJSON(`/api/agents/collections/${encodeURIComponent(name)}/entry/delete`, { method: 'DELETE', body: JSON.stringify({ entry }), headers: { 'Content-Type': 'application/json' } }),
|
||||
sources: (name) => fetchJSON(`/api/agents/collections/${encodeURIComponent(name)}/sources`),
|
||||
addSource: (name, url, interval) => postJSON(`/api/agents/collections/${encodeURIComponent(name)}/sources`, { url, update_interval: interval }),
|
||||
removeSource: (name, url) => fetchJSON(`/api/agents/collections/${encodeURIComponent(name)}/sources`, { method: 'DELETE', body: JSON.stringify({ url }), headers: { 'Content-Type': 'application/json' } }),
|
||||
}
|
||||
|
||||
// Skills API
|
||||
export const skillsApi = {
|
||||
list: () => fetchJSON('/api/agents/skills'),
|
||||
search: (q) => fetchJSON(`/api/agents/skills/search?q=${encodeURIComponent(q)}`),
|
||||
get: (name) => fetchJSON(`/api/agents/skills/${encodeURIComponent(name)}`),
|
||||
create: (data) => postJSON('/api/agents/skills', data),
|
||||
update: (name, data) => fetchJSON(`/api/agents/skills/${encodeURIComponent(name)}`, { method: 'PUT', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }),
|
||||
delete: (name) => fetchJSON(`/api/agents/skills/${encodeURIComponent(name)}`, { method: 'DELETE' }),
|
||||
import: (file) => { const fd = new FormData(); fd.append('file', file); return fetch('/api/agents/skills/import', { method: 'POST', body: fd }).then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }); },
|
||||
exportUrl: (name) => `/api/agents/skills/export/${encodeURIComponent(name)}`,
|
||||
listResources: (name) => fetchJSON(`/api/agents/skills/${encodeURIComponent(name)}/resources`),
|
||||
getResource: (name, path, opts) => fetchJSON(`/api/agents/skills/${encodeURIComponent(name)}/resources/${path}${opts?.json ? '?encoding=base64' : ''}`),
|
||||
createResource: (name, path, file) => { const fd = new FormData(); fd.append('file', file); fd.append('path', path); return fetch(`/api/agents/skills/${encodeURIComponent(name)}/resources`, { method: 'POST', body: fd }).then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }); },
|
||||
updateResource: (name, path, content) => postJSON(`/api/agents/skills/${encodeURIComponent(name)}/resources/${path}`, { content }),
|
||||
deleteResource: (name, path) => fetchJSON(`/api/agents/skills/${encodeURIComponent(name)}/resources/${path}`, { method: 'DELETE' }),
|
||||
listGitRepos: () => fetchJSON('/api/agents/git-repos'),
|
||||
addGitRepo: (url) => postJSON('/api/agents/git-repos', { url }),
|
||||
syncGitRepo: (id) => postJSON(`/api/agents/git-repos/${encodeURIComponent(id)}/sync`, {}),
|
||||
toggleGitRepo: (id) => postJSON(`/api/agents/git-repos/${encodeURIComponent(id)}/toggle`, {}),
|
||||
deleteGitRepo: (id) => fetchJSON(`/api/agents/git-repos/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
||||
}
|
||||
|
||||
// File to base64 helper
|
||||
export function fileToBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
73
core/http/routes/agents.go
Normal file
73
core/http/routes/agents.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mudler/LocalAI/core/application"
|
||||
"github.com/mudler/LocalAI/core/http/endpoints/localai"
|
||||
)
|
||||
|
||||
func RegisterAgentPoolRoutes(e *echo.Echo, app *application.Application) {
|
||||
if app.AgentPoolService() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Agent Management
|
||||
e.GET("/api/agents", localai.ListAgentsEndpoint(app))
|
||||
e.POST("/api/agents", localai.CreateAgentEndpoint(app))
|
||||
e.GET("/api/agents/config/metadata", localai.GetAgentConfigMetaEndpoint(app))
|
||||
e.POST("/api/agents/import", localai.ImportAgentEndpoint(app))
|
||||
e.GET("/api/agents/:name", localai.GetAgentEndpoint(app))
|
||||
e.PUT("/api/agents/:name", localai.UpdateAgentEndpoint(app))
|
||||
e.DELETE("/api/agents/:name", localai.DeleteAgentEndpoint(app))
|
||||
e.GET("/api/agents/:name/config", localai.GetAgentConfigEndpoint(app))
|
||||
e.PUT("/api/agents/:name/pause", localai.PauseAgentEndpoint(app))
|
||||
e.PUT("/api/agents/:name/resume", localai.ResumeAgentEndpoint(app))
|
||||
e.GET("/api/agents/:name/status", localai.GetAgentStatusEndpoint(app))
|
||||
e.GET("/api/agents/:name/observables", localai.GetAgentObservablesEndpoint(app))
|
||||
e.DELETE("/api/agents/:name/observables", localai.ClearAgentObservablesEndpoint(app))
|
||||
e.POST("/api/agents/:name/chat", localai.ChatWithAgentEndpoint(app))
|
||||
e.GET("/api/agents/:name/sse", localai.AgentSSEEndpoint(app))
|
||||
e.GET("/api/agents/:name/export", localai.ExportAgentEndpoint(app))
|
||||
|
||||
// Actions
|
||||
e.GET("/api/agents/actions", localai.ListActionsEndpoint(app))
|
||||
e.POST("/api/agents/actions/:name/definition", localai.GetActionDefinitionEndpoint(app))
|
||||
e.POST("/api/agents/actions/:name/run", localai.ExecuteActionEndpoint(app))
|
||||
|
||||
// Skills
|
||||
e.GET("/api/agents/skills", localai.ListSkillsEndpoint(app))
|
||||
e.GET("/api/agents/skills/config", localai.GetSkillsConfigEndpoint(app))
|
||||
e.GET("/api/agents/skills/search", localai.SearchSkillsEndpoint(app))
|
||||
e.POST("/api/agents/skills", localai.CreateSkillEndpoint(app))
|
||||
e.GET("/api/agents/skills/export/*", localai.ExportSkillEndpoint(app))
|
||||
e.POST("/api/agents/skills/import", localai.ImportSkillEndpoint(app))
|
||||
e.GET("/api/agents/skills/:name", localai.GetSkillEndpoint(app))
|
||||
e.PUT("/api/agents/skills/:name", localai.UpdateSkillEndpoint(app))
|
||||
e.DELETE("/api/agents/skills/:name", localai.DeleteSkillEndpoint(app))
|
||||
e.GET("/api/agents/skills/:name/resources", localai.ListSkillResourcesEndpoint(app))
|
||||
e.GET("/api/agents/skills/:name/resources/*", localai.GetSkillResourceEndpoint(app))
|
||||
e.POST("/api/agents/skills/:name/resources", localai.CreateSkillResourceEndpoint(app))
|
||||
e.PUT("/api/agents/skills/:name/resources/*", localai.UpdateSkillResourceEndpoint(app))
|
||||
e.DELETE("/api/agents/skills/:name/resources/*", localai.DeleteSkillResourceEndpoint(app))
|
||||
|
||||
// Git Repos
|
||||
e.GET("/api/agents/git-repos", localai.ListGitReposEndpoint(app))
|
||||
e.POST("/api/agents/git-repos", localai.AddGitRepoEndpoint(app))
|
||||
e.PUT("/api/agents/git-repos/:id", localai.UpdateGitRepoEndpoint(app))
|
||||
e.DELETE("/api/agents/git-repos/:id", localai.DeleteGitRepoEndpoint(app))
|
||||
e.POST("/api/agents/git-repos/:id/sync", localai.SyncGitRepoEndpoint(app))
|
||||
e.POST("/api/agents/git-repos/:id/toggle", localai.ToggleGitRepoEndpoint(app))
|
||||
|
||||
// Collections / Knowledge Base
|
||||
e.GET("/api/agents/collections", localai.ListCollectionsEndpoint(app))
|
||||
e.POST("/api/agents/collections", localai.CreateCollectionEndpoint(app))
|
||||
e.POST("/api/agents/collections/:name/upload", localai.UploadToCollectionEndpoint(app))
|
||||
e.GET("/api/agents/collections/:name/entries", localai.ListCollectionEntriesEndpoint(app))
|
||||
e.GET("/api/agents/collections/:name/entries/*", localai.GetCollectionEntryContentEndpoint(app))
|
||||
e.POST("/api/agents/collections/:name/search", localai.SearchCollectionEndpoint(app))
|
||||
e.POST("/api/agents/collections/:name/reset", localai.ResetCollectionEndpoint(app))
|
||||
e.DELETE("/api/agents/collections/:name/entry/delete", localai.DeleteCollectionEntryEndpoint(app))
|
||||
e.POST("/api/agents/collections/:name/sources", localai.AddCollectionSourceEndpoint(app))
|
||||
e.DELETE("/api/agents/collections/:name/sources", localai.RemoveCollectionSourceEndpoint(app))
|
||||
e.GET("/api/agents/collections/:name/sources", localai.ListCollectionSourcesEndpoint(app))
|
||||
}
|
||||
@@ -129,6 +129,13 @@ func RegisterLocalAIRoutes(router *echo.Echo,
|
||||
}{Version: internal.PrintableVersion()})
|
||||
})
|
||||
|
||||
router.GET("/api/features", func(c echo.Context) error {
|
||||
return c.JSON(200, map[string]bool{
|
||||
"agents": app.AgentPoolService() != nil,
|
||||
"mcp": !appConfig.DisableMCP,
|
||||
})
|
||||
})
|
||||
|
||||
router.GET("/system", localai.SystemInformations(ml, appConfig))
|
||||
|
||||
// misc
|
||||
@@ -159,8 +166,8 @@ func RegisterLocalAIRoutes(router *echo.Echo,
|
||||
router.POST("/mcp/chat/completions", mcpStreamHandler, mcpStreamMiddleware...)
|
||||
}
|
||||
|
||||
// Agent job routes
|
||||
if app != nil && app.AgentJobService() != nil {
|
||||
// Agent job routes (MCP CI Jobs — requires MCP to be enabled)
|
||||
if app != nil && app.AgentJobService() != nil && !appConfig.DisableMCP {
|
||||
router.POST("/api/agent/tasks", localai.CreateTaskEndpoint(app))
|
||||
router.PUT("/api/agent/tasks/:id", localai.UpdateTaskEndpoint(app))
|
||||
router.DELETE("/api/agent/tasks/:id", localai.DeleteTaskEndpoint(app))
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mudler/LocalAI/core/application"
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
localai "github.com/mudler/LocalAI/core/http/endpoints/localai"
|
||||
"github.com/mudler/LocalAI/core/http/endpoints/openresponses"
|
||||
"github.com/mudler/LocalAI/core/http/middleware"
|
||||
"github.com/mudler/LocalAI/core/schema"
|
||||
@@ -22,6 +23,9 @@ func RegisterOpenResponsesRoutes(app *echo.Echo,
|
||||
)
|
||||
|
||||
responsesMiddleware := []echo.MiddlewareFunc{
|
||||
// Intercept requests where the model name matches an agent — route directly
|
||||
// to the agent pool without going through the model config resolution pipeline.
|
||||
localai.AgentResponsesInterceptor(application),
|
||||
middleware.TraceMiddleware(application),
|
||||
re.BuildFilteredFirstAvailableDefaultModel(config.BuildUsecaseFilterFn(config.FLAG_CHAT)),
|
||||
re.SetModelAndConfig(func() schema.LocalAIRequest { return new(schema.OpenResponsesRequest) }),
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/mudler/LocalAI/pkg/model"
|
||||
"github.com/mudler/LocalAI/pkg/xsync"
|
||||
"github.com/mudler/cogito"
|
||||
"github.com/mudler/cogito/clients"
|
||||
"github.com/mudler/xlog"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
@@ -806,7 +807,7 @@ func (s *AgentJobService) executeJobInternal(job schema.Job, task schema.Task, c
|
||||
}
|
||||
|
||||
// Create LLM client
|
||||
defaultLLM := cogito.NewOpenAILLM(modelConfig.Name, apiKey, "http://127.0.0.1:"+port)
|
||||
defaultLLM := clients.NewLocalAILLM(modelConfig.Name, apiKey, "http://127.0.0.1:"+port)
|
||||
|
||||
// Initialize traces slice
|
||||
job.Traces = []schema.JobTrace{}
|
||||
|
||||
983
core/services/agent_pool.go
Normal file
983
core/services/agent_pool.go
Normal file
@@ -0,0 +1,983 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/agent"
|
||||
"github.com/mudler/LocalAGI/core/sse"
|
||||
"github.com/mudler/LocalAGI/core/state"
|
||||
coreTypes "github.com/mudler/LocalAGI/core/types"
|
||||
agiServices "github.com/mudler/LocalAGI/services"
|
||||
"github.com/mudler/LocalAGI/services/skills"
|
||||
"github.com/mudler/LocalAGI/webui/collections"
|
||||
"github.com/mudler/xlog"
|
||||
|
||||
skilldomain "github.com/mudler/skillserver/pkg/domain"
|
||||
skillgit "github.com/mudler/skillserver/pkg/git"
|
||||
)
|
||||
|
||||
// AgentPoolService wraps LocalAGI's AgentPool, Skills service, and collections backend
|
||||
// to provide agentic capabilities integrated directly into LocalAI.
|
||||
type AgentPoolService struct {
|
||||
appConfig *config.ApplicationConfig
|
||||
pool *state.AgentPool
|
||||
skillsService *skills.Service
|
||||
collectionsBackend collections.Backend
|
||||
configMeta state.AgentConfigMeta
|
||||
actionsConfig map[string]string
|
||||
sharedState *coreTypes.AgentSharedState
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewAgentPoolService(appConfig *config.ApplicationConfig) (*AgentPoolService, error) {
|
||||
return &AgentPoolService{
|
||||
appConfig: appConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) Start(ctx context.Context) error {
|
||||
cfg := s.appConfig.AgentPool
|
||||
|
||||
// API URL: use configured value, or derive self-referencing URL from LocalAI's address
|
||||
apiURL := cfg.APIURL
|
||||
if apiURL == "" {
|
||||
_, port, err := net.SplitHostPort(s.appConfig.APIAddress)
|
||||
if err != nil {
|
||||
port = strings.TrimPrefix(s.appConfig.APIAddress, ":")
|
||||
}
|
||||
apiURL = "http://127.0.0.1:" + port
|
||||
}
|
||||
apiKey := cfg.APIKey
|
||||
if apiKey == "" && len(s.appConfig.ApiKeys) > 0 {
|
||||
apiKey = s.appConfig.ApiKeys[0]
|
||||
}
|
||||
|
||||
// State dir defaults to DynamicConfigsDir (LocalAI configuration folder)
|
||||
stateDir := cfg.StateDir
|
||||
if stateDir == "" {
|
||||
stateDir = s.appConfig.DynamicConfigsDir
|
||||
}
|
||||
if stateDir == "" {
|
||||
stateDir = "agents"
|
||||
}
|
||||
if err := os.MkdirAll(stateDir, 0750); err != nil {
|
||||
return fmt.Errorf("failed to create agent pool state dir: %w", err)
|
||||
}
|
||||
|
||||
// Collections paths
|
||||
collectionDBPath := cfg.CollectionDBPath
|
||||
if collectionDBPath == "" {
|
||||
collectionDBPath = filepath.Join(stateDir, "collections")
|
||||
}
|
||||
fileAssets := filepath.Join(stateDir, "assets")
|
||||
|
||||
// Skills service — always created since the agent pool calls GetSkillsPrompt unconditionally.
|
||||
// When EnableSkills is false, the service still exists but the skills directory will be empty.
|
||||
skillsSvc, err := skills.NewService(stateDir)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to create skills service", "error", err)
|
||||
}
|
||||
s.skillsService = skillsSvc
|
||||
|
||||
// Actions config map — only set CustomActionsDir if non-empty to avoid
|
||||
// "open : no such file or directory" errors
|
||||
actionsConfig := map[string]string{
|
||||
agiServices.ConfigStateDir: stateDir,
|
||||
}
|
||||
if cfg.CustomActionsDir != "" {
|
||||
actionsConfig[agiServices.CustomActionsDir] = cfg.CustomActionsDir
|
||||
}
|
||||
|
||||
s.actionsConfig = actionsConfig
|
||||
s.sharedState = coreTypes.NewAgentSharedState(5 * time.Minute)
|
||||
|
||||
// Create the agent pool
|
||||
pool, err := state.NewAgentPool(
|
||||
cfg.DefaultModel,
|
||||
cfg.MultimodalModel,
|
||||
cfg.TranscriptionModel,
|
||||
cfg.TranscriptionLanguage,
|
||||
cfg.TTSModel,
|
||||
apiURL,
|
||||
apiKey,
|
||||
stateDir,
|
||||
agiServices.Actions(actionsConfig),
|
||||
agiServices.Connectors,
|
||||
agiServices.DynamicPrompts(actionsConfig),
|
||||
agiServices.Filters,
|
||||
cfg.Timeout,
|
||||
cfg.EnableLogs,
|
||||
skillsSvc,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create agent pool: %w", err)
|
||||
}
|
||||
s.pool = pool
|
||||
|
||||
// Create in-process collections backend and RAG provider directly
|
||||
collectionsCfg := &collections.Config{
|
||||
LLMAPIURL: apiURL,
|
||||
LLMAPIKey: apiKey,
|
||||
LLMModel: cfg.DefaultModel,
|
||||
CollectionDBPath: collectionDBPath,
|
||||
FileAssets: fileAssets,
|
||||
VectorEngine: cfg.VectorEngine,
|
||||
EmbeddingModel: cfg.EmbeddingModel,
|
||||
MaxChunkingSize: cfg.MaxChunkingSize,
|
||||
ChunkOverlap: cfg.ChunkOverlap,
|
||||
DatabaseURL: cfg.DatabaseURL,
|
||||
}
|
||||
collectionsBackend, collectionsState := collections.NewInProcessBackend(collectionsCfg)
|
||||
s.collectionsBackend = collectionsBackend
|
||||
|
||||
// Set up in-process RAG provider from collections state
|
||||
embedded := collections.RAGProviderFromState(collectionsState)
|
||||
pool.SetRAGProvider(func(collectionName, _, _ string) (agent.RAGDB, state.KBCompactionClient, bool) {
|
||||
return embedded(collectionName)
|
||||
})
|
||||
|
||||
// Build config metadata for UI
|
||||
s.configMeta = state.NewAgentConfigMeta(
|
||||
agiServices.ActionsConfigMeta(cfg.CustomActionsDir),
|
||||
agiServices.ConnectorsConfigMeta(),
|
||||
agiServices.DynamicPromptsConfigMeta(cfg.CustomActionsDir),
|
||||
agiServices.FiltersConfigMeta(),
|
||||
)
|
||||
|
||||
// Start all agents
|
||||
if err := pool.StartAll(); err != nil {
|
||||
xlog.Error("Failed to start agent pool", "error", err)
|
||||
}
|
||||
|
||||
xlog.Info("Agent pool started", "stateDir", stateDir, "apiURL", apiURL)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) Stop() {
|
||||
if s.pool != nil {
|
||||
s.pool.StopAll()
|
||||
}
|
||||
}
|
||||
|
||||
// Pool returns the underlying AgentPool.
|
||||
func (s *AgentPoolService) Pool() *state.AgentPool {
|
||||
return s.pool
|
||||
}
|
||||
|
||||
// --- Agent CRUD ---
|
||||
|
||||
func (s *AgentPoolService) ListAgents() map[string]bool {
|
||||
statuses := map[string]bool{}
|
||||
agents := s.pool.List()
|
||||
for _, a := range agents {
|
||||
ag := s.pool.GetAgent(a)
|
||||
if ag == nil {
|
||||
continue
|
||||
}
|
||||
statuses[a] = !ag.Paused()
|
||||
}
|
||||
return statuses
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) CreateAgent(config *state.AgentConfig) error {
|
||||
if config.Name == "" {
|
||||
return fmt.Errorf("name is required")
|
||||
}
|
||||
return s.pool.CreateAgent(config.Name, config)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) GetAgent(name string) *agent.Agent {
|
||||
return s.pool.GetAgent(name)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) GetAgentConfig(name string) *state.AgentConfig {
|
||||
return s.pool.GetConfig(name)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) UpdateAgent(name string, config *state.AgentConfig) error {
|
||||
old := s.pool.GetConfig(name)
|
||||
if old == nil {
|
||||
return fmt.Errorf("agent not found: %s", name)
|
||||
}
|
||||
return s.pool.RecreateAgent(name, config)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) DeleteAgent(name string) error {
|
||||
return s.pool.Remove(name)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) PauseAgent(name string) error {
|
||||
ag := s.pool.GetAgent(name)
|
||||
if ag == nil {
|
||||
return fmt.Errorf("agent not found: %s", name)
|
||||
}
|
||||
ag.Pause()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) ResumeAgent(name string) error {
|
||||
ag := s.pool.GetAgent(name)
|
||||
if ag == nil {
|
||||
return fmt.Errorf("agent not found: %s", name)
|
||||
}
|
||||
ag.Resume()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) GetAgentStatus(name string) *state.Status {
|
||||
return s.pool.GetStatusHistory(name)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) GetAgentObservables(name string) ([]coreTypes.Observable, error) {
|
||||
ag := s.pool.GetAgent(name)
|
||||
if ag == nil {
|
||||
return nil, fmt.Errorf("agent not found: %s", name)
|
||||
}
|
||||
return ag.Observer().History(), nil
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) ClearAgentObservables(name string) error {
|
||||
ag := s.pool.GetAgent(name)
|
||||
if ag == nil {
|
||||
return fmt.Errorf("agent not found: %s", name)
|
||||
}
|
||||
ag.Observer().ClearHistory()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Chat sends a message to an agent and returns immediately. Responses come via SSE.
|
||||
func (s *AgentPoolService) Chat(name, message string) (string, error) {
|
||||
ag := s.pool.GetAgent(name)
|
||||
if ag == nil {
|
||||
return "", fmt.Errorf("agent not found: %s", name)
|
||||
}
|
||||
manager := s.pool.GetManager(name)
|
||||
if manager == nil {
|
||||
return "", fmt.Errorf("SSE manager not found for agent: %s", name)
|
||||
}
|
||||
|
||||
messageID := fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
|
||||
// Send user message via SSE
|
||||
userMsg, _ := json.Marshal(map[string]any{
|
||||
"id": messageID + "-user",
|
||||
"sender": "user",
|
||||
"content": message,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
manager.Send(sse.NewMessage(string(userMsg)).WithEvent("json_message"))
|
||||
|
||||
// Send processing status
|
||||
statusMsg, _ := json.Marshal(map[string]any{
|
||||
"status": "processing",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
manager.Send(sse.NewMessage(string(statusMsg)).WithEvent("json_message_status"))
|
||||
|
||||
// Process asynchronously
|
||||
go func() {
|
||||
response := ag.Ask(coreTypes.WithText(message))
|
||||
|
||||
if response == nil {
|
||||
errMsg, _ := json.Marshal(map[string]any{
|
||||
"error": "agent request failed or was cancelled",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
manager.Send(sse.NewMessage(string(errMsg)).WithEvent("json_error"))
|
||||
} else if response.Error != nil {
|
||||
errMsg, _ := json.Marshal(map[string]any{
|
||||
"error": response.Error.Error(),
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
manager.Send(sse.NewMessage(string(errMsg)).WithEvent("json_error"))
|
||||
} else {
|
||||
respMsg, _ := json.Marshal(map[string]any{
|
||||
"id": messageID + "-agent",
|
||||
"sender": "agent",
|
||||
"content": response.Response,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
manager.Send(sse.NewMessage(string(respMsg)).WithEvent("json_message"))
|
||||
}
|
||||
|
||||
completedMsg, _ := json.Marshal(map[string]any{
|
||||
"status": "completed",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
manager.Send(sse.NewMessage(string(completedMsg)).WithEvent("json_message_status"))
|
||||
}()
|
||||
|
||||
return messageID, nil
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) GetSSEManager(name string) sse.Manager {
|
||||
return s.pool.GetManager(name)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) GetConfigMeta() state.AgentConfigMeta {
|
||||
return s.configMeta
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) AgentHubURL() string {
|
||||
return s.appConfig.AgentPool.AgentHubURL
|
||||
}
|
||||
|
||||
// ExportAgent returns the agent config as JSON bytes.
|
||||
func (s *AgentPoolService) ExportAgent(name string) ([]byte, error) {
|
||||
cfg := s.pool.GetConfig(name)
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("agent not found: %s", name)
|
||||
}
|
||||
return json.MarshalIndent(cfg, "", " ")
|
||||
}
|
||||
|
||||
// ImportAgent creates an agent from JSON config data.
|
||||
func (s *AgentPoolService) ImportAgent(data []byte) error {
|
||||
var cfg state.AgentConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return fmt.Errorf("invalid agent config: %w", err)
|
||||
}
|
||||
if cfg.Name == "" {
|
||||
return fmt.Errorf("agent name is required")
|
||||
}
|
||||
return s.pool.CreateAgent(cfg.Name, &cfg)
|
||||
}
|
||||
|
||||
// --- Skills ---
|
||||
|
||||
func (s *AgentPoolService) SkillsService() *skills.Service {
|
||||
return s.skillsService
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) GetSkillsConfig() map[string]any {
|
||||
if s.skillsService == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{"skills_dir": s.skillsService.GetSkillsDir()}
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) ListSkills() ([]skilldomain.Skill, error) {
|
||||
if s.skillsService == nil {
|
||||
return nil, fmt.Errorf("skills service not available")
|
||||
}
|
||||
mgr, err := s.skillsService.GetManager()
|
||||
if err != nil || mgr == nil {
|
||||
if mgr == nil {
|
||||
return []skilldomain.Skill{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return mgr.ListSkills()
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) GetSkill(name string) (*skilldomain.Skill, error) {
|
||||
if s.skillsService == nil {
|
||||
return nil, fmt.Errorf("skills service not available")
|
||||
}
|
||||
mgr, err := s.skillsService.GetManager()
|
||||
if err != nil || mgr == nil {
|
||||
return nil, fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
return mgr.ReadSkill(name)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) SearchSkills(query string) ([]skilldomain.Skill, error) {
|
||||
if s.skillsService == nil {
|
||||
return nil, fmt.Errorf("skills service not available")
|
||||
}
|
||||
mgr, err := s.skillsService.GetManager()
|
||||
if err != nil || mgr == nil {
|
||||
return nil, fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
return mgr.SearchSkills(query)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) CreateSkill(name, description, content, license, compatibility, allowedTools string, metadata map[string]string) (*skilldomain.Skill, error) {
|
||||
if s.skillsService == nil {
|
||||
return nil, fmt.Errorf("skills service not available")
|
||||
}
|
||||
mgr, err := s.skillsService.GetManager()
|
||||
if err != nil || mgr == nil {
|
||||
return nil, fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
fsManager, ok := mgr.(*skilldomain.FileSystemManager)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported manager type")
|
||||
}
|
||||
if err := skilldomain.ValidateSkillName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
skillsDir := fsManager.GetSkillsDir()
|
||||
skillDir := filepath.Join(skillsDir, name)
|
||||
if _, err := os.Stat(skillDir); err == nil {
|
||||
return nil, fmt.Errorf("skill already exists")
|
||||
}
|
||||
if err := os.MkdirAll(skillDir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
frontmatter := fmt.Sprintf("---\nname: %s\ndescription: %s\n", name, description)
|
||||
if license != "" {
|
||||
frontmatter += fmt.Sprintf("license: %s\n", license)
|
||||
}
|
||||
if compatibility != "" {
|
||||
frontmatter += fmt.Sprintf("compatibility: %s\n", compatibility)
|
||||
}
|
||||
if len(metadata) > 0 {
|
||||
frontmatter += "metadata:\n"
|
||||
for k, v := range metadata {
|
||||
frontmatter += fmt.Sprintf(" %s: %s\n", k, v)
|
||||
}
|
||||
}
|
||||
if allowedTools != "" {
|
||||
frontmatter += fmt.Sprintf("allowed-tools: %s\n", allowedTools)
|
||||
}
|
||||
frontmatter += "---\n\n"
|
||||
|
||||
if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(frontmatter+content), 0644); err != nil {
|
||||
os.RemoveAll(skillDir)
|
||||
return nil, err
|
||||
}
|
||||
if err := mgr.RebuildIndex(); err != nil {
|
||||
return nil, fmt.Errorf("failed to rebuild index: %w", err)
|
||||
}
|
||||
return mgr.ReadSkill(name)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) UpdateSkill(name, description, content, license, compatibility, allowedTools string, metadata map[string]string) (*skilldomain.Skill, error) {
|
||||
if s.skillsService == nil {
|
||||
return nil, fmt.Errorf("skills service not available")
|
||||
}
|
||||
mgr, err := s.skillsService.GetManager()
|
||||
if err != nil || mgr == nil {
|
||||
return nil, fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
fsManager, ok := mgr.(*skilldomain.FileSystemManager)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported manager type")
|
||||
}
|
||||
existing, err := mgr.ReadSkill(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("skill not found")
|
||||
}
|
||||
if existing.ReadOnly {
|
||||
return nil, fmt.Errorf("cannot update read-only skill from git repository")
|
||||
}
|
||||
|
||||
skillDir := filepath.Join(fsManager.GetSkillsDir(), name)
|
||||
frontmatter := fmt.Sprintf("---\nname: %s\ndescription: %s\n", name, description)
|
||||
if license != "" {
|
||||
frontmatter += fmt.Sprintf("license: %s\n", license)
|
||||
}
|
||||
if compatibility != "" {
|
||||
frontmatter += fmt.Sprintf("compatibility: %s\n", compatibility)
|
||||
}
|
||||
if len(metadata) > 0 {
|
||||
frontmatter += "metadata:\n"
|
||||
for k, v := range metadata {
|
||||
frontmatter += fmt.Sprintf(" %s: %s\n", k, v)
|
||||
}
|
||||
}
|
||||
if allowedTools != "" {
|
||||
frontmatter += fmt.Sprintf("allowed-tools: %s\n", allowedTools)
|
||||
}
|
||||
frontmatter += "---\n\n"
|
||||
|
||||
if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(frontmatter+content), 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mgr.RebuildIndex(); err != nil {
|
||||
return nil, fmt.Errorf("failed to rebuild index: %w", err)
|
||||
}
|
||||
return mgr.ReadSkill(name)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) DeleteSkill(name string) error {
|
||||
if s.skillsService == nil {
|
||||
return fmt.Errorf("skills service not available")
|
||||
}
|
||||
mgr, err := s.skillsService.GetManager()
|
||||
if err != nil || mgr == nil {
|
||||
return fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
fsManager, ok := mgr.(*skilldomain.FileSystemManager)
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported manager type")
|
||||
}
|
||||
existing, err := mgr.ReadSkill(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("skill not found")
|
||||
}
|
||||
if existing.ReadOnly {
|
||||
return fmt.Errorf("cannot delete read-only skill from git repository")
|
||||
}
|
||||
skillDir := filepath.Join(fsManager.GetSkillsDir(), name)
|
||||
if err := os.RemoveAll(skillDir); err != nil {
|
||||
return err
|
||||
}
|
||||
return mgr.RebuildIndex()
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) ExportSkill(name string) ([]byte, error) {
|
||||
if s.skillsService == nil {
|
||||
return nil, fmt.Errorf("skills service not available")
|
||||
}
|
||||
mgr, err := s.skillsService.GetManager()
|
||||
if err != nil || mgr == nil {
|
||||
return nil, fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
fsManager, ok := mgr.(*skilldomain.FileSystemManager)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported manager type")
|
||||
}
|
||||
skill, err := mgr.ReadSkill(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("skill not found")
|
||||
}
|
||||
return skilldomain.ExportSkill(skill.ID, fsManager.GetSkillsDir())
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) ImportSkill(archiveData []byte) (*skilldomain.Skill, error) {
|
||||
if s.skillsService == nil {
|
||||
return nil, fmt.Errorf("skills service not available")
|
||||
}
|
||||
mgr, err := s.skillsService.GetManager()
|
||||
if err != nil || mgr == nil {
|
||||
return nil, fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
fsManager, ok := mgr.(*skilldomain.FileSystemManager)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported manager type")
|
||||
}
|
||||
skillName, err := skilldomain.ImportSkill(archiveData, fsManager.GetSkillsDir())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mgr.RebuildIndex(); err != nil {
|
||||
return nil, fmt.Errorf("failed to rebuild index: %w", err)
|
||||
}
|
||||
return mgr.ReadSkill(skillName)
|
||||
}
|
||||
|
||||
// --- Skill Resources ---
|
||||
|
||||
func (s *AgentPoolService) ListSkillResources(skillName string) ([]skilldomain.SkillResource, *skilldomain.Skill, error) {
|
||||
if s.skillsService == nil {
|
||||
return nil, nil, fmt.Errorf("skills service not available")
|
||||
}
|
||||
mgr, err := s.skillsService.GetManager()
|
||||
if err != nil || mgr == nil {
|
||||
return nil, nil, fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
skill, err := mgr.ReadSkill(skillName)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("skill not found")
|
||||
}
|
||||
resources, err := mgr.ListSkillResources(skill.ID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return resources, skill, nil
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) GetSkillResource(skillName, resourcePath string) (*skilldomain.ResourceContent, *skilldomain.SkillResource, error) {
|
||||
if s.skillsService == nil {
|
||||
return nil, nil, fmt.Errorf("skills service not available")
|
||||
}
|
||||
mgr, err := s.skillsService.GetManager()
|
||||
if err != nil || mgr == nil {
|
||||
return nil, nil, fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
skill, err := mgr.ReadSkill(skillName)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("skill not found")
|
||||
}
|
||||
info, err := mgr.GetSkillResourceInfo(skill.ID, resourcePath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("resource not found")
|
||||
}
|
||||
content, err := mgr.ReadSkillResource(skill.ID, resourcePath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return content, info, nil
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) CreateSkillResource(skillName, path string, data []byte) error {
|
||||
if s.skillsService == nil {
|
||||
return fmt.Errorf("skills service not available")
|
||||
}
|
||||
mgr, err := s.skillsService.GetManager()
|
||||
if err != nil || mgr == nil {
|
||||
return fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
skill, err := mgr.ReadSkill(skillName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("skill not found")
|
||||
}
|
||||
if skill.ReadOnly {
|
||||
return fmt.Errorf("cannot add resources to read-only skill")
|
||||
}
|
||||
if err := skilldomain.ValidateResourcePath(path); err != nil {
|
||||
return err
|
||||
}
|
||||
fullPath := filepath.Join(skill.SourcePath, path)
|
||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(fullPath, data, 0644)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) UpdateSkillResource(skillName, resourcePath, content string) error {
|
||||
if s.skillsService == nil {
|
||||
return fmt.Errorf("skills service not available")
|
||||
}
|
||||
mgr, err := s.skillsService.GetManager()
|
||||
if err != nil || mgr == nil {
|
||||
return fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
skill, err := mgr.ReadSkill(skillName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("skill not found")
|
||||
}
|
||||
if skill.ReadOnly {
|
||||
return fmt.Errorf("cannot update resources in read-only skill")
|
||||
}
|
||||
if err := skilldomain.ValidateResourcePath(resourcePath); err != nil {
|
||||
return err
|
||||
}
|
||||
fullPath := filepath.Join(skill.SourcePath, resourcePath)
|
||||
return os.WriteFile(fullPath, []byte(content), 0644)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) DeleteSkillResource(skillName, resourcePath string) error {
|
||||
if s.skillsService == nil {
|
||||
return fmt.Errorf("skills service not available")
|
||||
}
|
||||
mgr, err := s.skillsService.GetManager()
|
||||
if err != nil || mgr == nil {
|
||||
return fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
skill, err := mgr.ReadSkill(skillName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("skill not found")
|
||||
}
|
||||
if skill.ReadOnly {
|
||||
return fmt.Errorf("cannot delete resources from read-only skill")
|
||||
}
|
||||
if err := skilldomain.ValidateResourcePath(resourcePath); err != nil {
|
||||
return err
|
||||
}
|
||||
fullPath := filepath.Join(skill.SourcePath, resourcePath)
|
||||
return os.Remove(fullPath)
|
||||
}
|
||||
|
||||
// --- Git Repos ---
|
||||
|
||||
func (s *AgentPoolService) getSkillsDir() string {
|
||||
if s.skillsService == nil {
|
||||
return ""
|
||||
}
|
||||
return s.skillsService.GetSkillsDir()
|
||||
}
|
||||
|
||||
type GitRepoInfo struct {
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) ListGitRepos() ([]GitRepoInfo, error) {
|
||||
dir := s.getSkillsDir()
|
||||
if dir == "" {
|
||||
return []GitRepoInfo{}, nil
|
||||
}
|
||||
cm := skillgit.NewConfigManager(dir)
|
||||
repos, err := cm.LoadConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]GitRepoInfo, len(repos))
|
||||
for i, r := range repos {
|
||||
out[i] = GitRepoInfo{ID: r.ID, URL: r.URL, Name: r.Name, Enabled: r.Enabled}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) AddGitRepo(repoURL string) (*GitRepoInfo, error) {
|
||||
dir := s.getSkillsDir()
|
||||
if dir == "" {
|
||||
return nil, fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
if !strings.HasPrefix(repoURL, "http://") && !strings.HasPrefix(repoURL, "https://") && !strings.HasPrefix(repoURL, "git@") {
|
||||
return nil, fmt.Errorf("invalid URL format")
|
||||
}
|
||||
cm := skillgit.NewConfigManager(dir)
|
||||
repos, err := cm.LoadConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range repos {
|
||||
if r.URL == repoURL {
|
||||
return nil, fmt.Errorf("repository already exists")
|
||||
}
|
||||
}
|
||||
newRepo := skillgit.GitRepoConfig{
|
||||
ID: skillgit.GenerateID(repoURL),
|
||||
URL: repoURL,
|
||||
Name: skillgit.ExtractRepoName(repoURL),
|
||||
Enabled: true,
|
||||
}
|
||||
repos = append(repos, newRepo)
|
||||
if err := cm.SaveConfig(repos); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Background sync
|
||||
go func() {
|
||||
mgr, err := s.skillsService.GetManager()
|
||||
if err != nil || mgr == nil {
|
||||
return
|
||||
}
|
||||
syncer := skillgit.NewGitSyncer(dir, []string{repoURL}, mgr.RebuildIndex)
|
||||
if err := syncer.Start(); err != nil {
|
||||
xlog.Error("background sync failed", "url", repoURL, "error", err)
|
||||
s.skillsService.RefreshManagerFromConfig()
|
||||
return
|
||||
}
|
||||
syncer.Stop()
|
||||
s.skillsService.RefreshManagerFromConfig()
|
||||
}()
|
||||
|
||||
return &GitRepoInfo{ID: newRepo.ID, URL: newRepo.URL, Name: newRepo.Name, Enabled: newRepo.Enabled}, nil
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) UpdateGitRepo(id, repoURL string, enabled *bool) (*GitRepoInfo, error) {
|
||||
dir := s.getSkillsDir()
|
||||
if dir == "" {
|
||||
return nil, fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
cm := skillgit.NewConfigManager(dir)
|
||||
repos, err := cm.LoadConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idx := -1
|
||||
for i, r := range repos {
|
||||
if r.ID == id {
|
||||
idx = i
|
||||
if repoURL != "" {
|
||||
parsedURL, err := url.Parse(repoURL)
|
||||
if err != nil || parsedURL.Scheme == "" {
|
||||
return nil, fmt.Errorf("invalid repository URL")
|
||||
}
|
||||
repos[i].URL = repoURL
|
||||
repos[i].Name = skillgit.ExtractRepoName(repoURL)
|
||||
}
|
||||
if enabled != nil {
|
||||
repos[i].Enabled = *enabled
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
return nil, fmt.Errorf("repository not found")
|
||||
}
|
||||
if err := cm.SaveConfig(repos); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.skillsService.RefreshManagerFromConfig()
|
||||
r := repos[idx]
|
||||
return &GitRepoInfo{ID: r.ID, URL: r.URL, Name: r.Name, Enabled: r.Enabled}, nil
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) DeleteGitRepo(id string) error {
|
||||
dir := s.getSkillsDir()
|
||||
if dir == "" {
|
||||
return fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
cm := skillgit.NewConfigManager(dir)
|
||||
repos, err := cm.LoadConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var newRepos []skillgit.GitRepoConfig
|
||||
var repoName string
|
||||
for _, r := range repos {
|
||||
if r.ID == id {
|
||||
repoName = r.Name
|
||||
} else {
|
||||
newRepos = append(newRepos, r)
|
||||
}
|
||||
}
|
||||
if len(newRepos) == len(repos) {
|
||||
return fmt.Errorf("repository not found")
|
||||
}
|
||||
if err := cm.SaveConfig(newRepos); err != nil {
|
||||
return err
|
||||
}
|
||||
if repoName != "" {
|
||||
os.RemoveAll(filepath.Join(dir, repoName))
|
||||
}
|
||||
s.skillsService.RefreshManagerFromConfig()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) SyncGitRepo(id string) error {
|
||||
dir := s.getSkillsDir()
|
||||
if dir == "" {
|
||||
return fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
cm := skillgit.NewConfigManager(dir)
|
||||
repos, err := cm.LoadConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var repoURL string
|
||||
for _, r := range repos {
|
||||
if r.ID == id {
|
||||
repoURL = r.URL
|
||||
break
|
||||
}
|
||||
}
|
||||
if repoURL == "" {
|
||||
return fmt.Errorf("repository not found")
|
||||
}
|
||||
mgr, err := s.skillsService.GetManager()
|
||||
if err != nil || mgr == nil {
|
||||
return fmt.Errorf("manager not ready")
|
||||
}
|
||||
go func() {
|
||||
syncer := skillgit.NewGitSyncer(dir, []string{repoURL}, mgr.RebuildIndex)
|
||||
if err := syncer.Start(); err != nil {
|
||||
xlog.Error("background sync failed", "id", id, "error", err)
|
||||
s.skillsService.RefreshManagerFromConfig()
|
||||
return
|
||||
}
|
||||
syncer.Stop()
|
||||
s.skillsService.RefreshManagerFromConfig()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) ToggleGitRepo(id string) (*GitRepoInfo, error) {
|
||||
dir := s.getSkillsDir()
|
||||
if dir == "" {
|
||||
return nil, fmt.Errorf("skills directory not configured")
|
||||
}
|
||||
cm := skillgit.NewConfigManager(dir)
|
||||
repos, err := cm.LoadConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, r := range repos {
|
||||
if r.ID == id {
|
||||
repos[i].Enabled = !repos[i].Enabled
|
||||
if err := cm.SaveConfig(repos); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.skillsService.RefreshManagerFromConfig()
|
||||
return &GitRepoInfo{ID: repos[i].ID, URL: repos[i].URL, Name: repos[i].Name, Enabled: repos[i].Enabled}, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("repository not found")
|
||||
}
|
||||
|
||||
// --- Collections ---
|
||||
|
||||
func (s *AgentPoolService) CollectionsBackend() collections.Backend {
|
||||
return s.collectionsBackend
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) ListCollections() ([]string, error) {
|
||||
return s.collectionsBackend.ListCollections()
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) CreateCollection(name string) error {
|
||||
return s.collectionsBackend.CreateCollection(name)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) UploadToCollection(collection, filename string, fileBody io.Reader) error {
|
||||
return s.collectionsBackend.Upload(collection, filename, fileBody)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) ListCollectionEntries(collection string) ([]string, error) {
|
||||
return s.collectionsBackend.ListEntries(collection)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) GetCollectionEntryContent(collection, entry string) (string, int, error) {
|
||||
return s.collectionsBackend.GetEntryContent(collection, entry)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) SearchCollection(collection, query string, maxResults int) ([]collections.SearchResult, error) {
|
||||
return s.collectionsBackend.Search(collection, query, maxResults)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) ResetCollection(collection string) error {
|
||||
return s.collectionsBackend.Reset(collection)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) DeleteCollectionEntry(collection, entry string) ([]string, error) {
|
||||
return s.collectionsBackend.DeleteEntry(collection, entry)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) AddCollectionSource(collection, sourceURL string, intervalMin int) error {
|
||||
return s.collectionsBackend.AddSource(collection, sourceURL, intervalMin)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) RemoveCollectionSource(collection, sourceURL string) error {
|
||||
return s.collectionsBackend.RemoveSource(collection, sourceURL)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) ListCollectionSources(collection string) ([]collections.SourceInfo, error) {
|
||||
return s.collectionsBackend.ListSources(collection)
|
||||
}
|
||||
|
||||
func (s *AgentPoolService) CollectionEntryExists(collection, entry string) bool {
|
||||
return s.collectionsBackend.EntryExists(collection, entry)
|
||||
}
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
// ListAvailableActions returns the list of all available action type names.
|
||||
func (s *AgentPoolService) ListAvailableActions() []string {
|
||||
return agiServices.AvailableActions
|
||||
}
|
||||
|
||||
// GetActionDefinition creates an action instance by name with the given config and returns its definition.
|
||||
func (s *AgentPoolService) GetActionDefinition(actionName string, actionConfig map[string]string) (any, error) {
|
||||
if actionConfig == nil {
|
||||
actionConfig = map[string]string{}
|
||||
}
|
||||
a, err := agiServices.Action(actionName, "", actionConfig, s.pool, s.actionsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.Definition(), nil
|
||||
}
|
||||
|
||||
// ExecuteAction creates an action instance and runs it with the given params.
|
||||
func (s *AgentPoolService) ExecuteAction(ctx context.Context, actionName string, actionConfig map[string]string, params coreTypes.ActionParams) (coreTypes.ActionResult, error) {
|
||||
if actionConfig == nil {
|
||||
actionConfig = map[string]string{}
|
||||
}
|
||||
a, err := agiServices.Action(actionName, "", actionConfig, s.pool, s.actionsConfig)
|
||||
if err != nil {
|
||||
return coreTypes.ActionResult{}, err
|
||||
}
|
||||
return a.Run(ctx, s.sharedState, params)
|
||||
}
|
||||
53
core/services/agent_pool_sse.go
Normal file
53
core/services/agent_pool_sse.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mudler/LocalAGI/core/sse"
|
||||
)
|
||||
|
||||
// HandleSSE bridges a LocalAGI SSE Manager to an Echo HTTP response.
|
||||
// It registers a client with the manager, streams events, and cleans up on disconnect.
|
||||
func HandleSSE(c echo.Context, manager sse.Manager) error {
|
||||
c.Response().Header().Set("Content-Type", "text/event-stream")
|
||||
c.Response().Header().Set("Cache-Control", "no-cache")
|
||||
c.Response().Header().Set("Connection", "keep-alive")
|
||||
c.Response().WriteHeader(200)
|
||||
c.Response().Flush()
|
||||
|
||||
client := sse.NewClient(randString(10))
|
||||
manager.Register(client)
|
||||
defer func() {
|
||||
manager.Unregister(client.ID())
|
||||
}()
|
||||
|
||||
ch := client.Chan()
|
||||
done := c.Request().Context().Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return nil
|
||||
case msg, ok := <-ch:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if _, err := fmt.Fprint(c.Response(), msg.String()); err != nil {
|
||||
return nil
|
||||
}
|
||||
c.Response().Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
func randString(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -19,6 +19,15 @@ services:
|
||||
environment:
|
||||
- MODELS_PATH=/models
|
||||
# - DEBUG=true
|
||||
## Agents (LocalAGI) - https://localai.io/features/agents/
|
||||
# - LOCALAI_DISABLE_AGENTS=false
|
||||
# - LOCALAI_AGENT_POOL_DEFAULT_MODEL=hermes-3-llama3.1-8b
|
||||
# - LOCALAI_AGENT_POOL_ENABLE_SKILLS=true
|
||||
# - LOCALAI_AGENT_POOL_ENABLE_LOGS=true
|
||||
# - LOCALAI_AGENT_HUB_URL=https://agenthub.localai.io
|
||||
## Uncomment to use PostgreSQL for the knowledge base (requires the postgres service below)
|
||||
# - LOCALAI_AGENT_POOL_VECTOR_ENGINE=postgres
|
||||
# - LOCALAI_AGENT_POOL_DATABASE_URL=postgresql://localrecall:localrecall@postgres:5432/localrecall?sslmode=disable
|
||||
volumes:
|
||||
- models:/models
|
||||
- images:/tmp/generated/images/
|
||||
@@ -46,6 +55,22 @@ services:
|
||||
# count: 1
|
||||
# capabilities: [gpu]
|
||||
|
||||
## Uncomment for PostgreSQL-backed knowledge base (see Agents docs)
|
||||
# postgres:
|
||||
# image: quay.io/mudler/localrecall:v0.5.2-postgresql
|
||||
# environment:
|
||||
# - POSTGRES_DB=localrecall
|
||||
# - POSTGRES_USER=localrecall
|
||||
# - POSTGRES_PASSWORD=localrecall
|
||||
# volumes:
|
||||
# - postgres_data:/var/lib/postgresql/data
|
||||
# healthcheck:
|
||||
# test: ["CMD-SHELL", "pg_isready -U localrecall"]
|
||||
# interval: 10s
|
||||
# timeout: 5s
|
||||
# retries: 5
|
||||
|
||||
volumes:
|
||||
models:
|
||||
images:
|
||||
# postgres_data:
|
||||
|
||||
@@ -25,6 +25,7 @@ LocalAI provides a comprehensive set of features for running AI models locally.
|
||||
- **[GPU Acceleration](GPU-acceleration/)** - Optimize performance with GPU support
|
||||
- **[Distributed Inference](distributed_inferencing/)** - Scale inference across multiple nodes
|
||||
- **[Model Context Protocol (MCP)](mcp/)** - Enable agentic capabilities with MCP integration
|
||||
- **[Agents](agents/)** - Autonomous AI agents with tools, knowledge base, and skills
|
||||
|
||||
## Specialized Features
|
||||
|
||||
|
||||
315
docs/content/features/agents.md
Normal file
315
docs/content/features/agents.md
Normal file
@@ -0,0 +1,315 @@
|
||||
+++
|
||||
disableToc = false
|
||||
title = "🤖 Agents"
|
||||
weight = 21
|
||||
url = '/features/agents'
|
||||
+++
|
||||
|
||||
LocalAI includes a built-in agent platform powered by [LocalAGI](https://github.com/mudler/LocalAGI). Agents are autonomous AI entities that can reason, use tools, maintain memory, and interact with external services — all running locally as part of the LocalAI process.
|
||||
|
||||
## Overview
|
||||
|
||||
The agent system provides:
|
||||
|
||||
- **Autonomous agents** with configurable goals, personalities, and capabilities
|
||||
- **Tool/Action support** — agents can execute actions (web search, code execution, API calls, etc.)
|
||||
- **Knowledge base (RAG)** — per-agent collections with document upload, chunking, and semantic search
|
||||
- **Skills system** — reusable skill definitions that agents can leverage, with git-based skill repositories
|
||||
- **SSE streaming** — real-time chat with agents via Server-Sent Events
|
||||
- **Import/Export** — share agent configurations as JSON files
|
||||
- **Agent Hub** — browse and download ready-made agents from [agenthub.localai.io](https://agenthub.localai.io)
|
||||
- **Web UI** — full management interface for creating, editing, chatting with, and monitoring agents
|
||||
|
||||
## Getting Started
|
||||
|
||||
Agents are enabled by default. To disable them, set:
|
||||
|
||||
```bash
|
||||
LOCALAI_DISABLE_AGENTS=true
|
||||
```
|
||||
|
||||
### Creating an Agent
|
||||
|
||||
1. Navigate to the **Agents** page in the web UI
|
||||
2. Click **Create Agent** or import one from the [Agent Hub](https://agenthub.localai.io)
|
||||
3. Configure the agent's name, model, system prompt, and actions
|
||||
4. Save and start chatting
|
||||
|
||||
### Importing an Agent
|
||||
|
||||
You can import agent configurations from JSON files:
|
||||
|
||||
1. Download an agent configuration from the [Agent Hub](https://agenthub.localai.io) or export one from another LocalAI instance
|
||||
2. On the **Agents** page, click **Import**
|
||||
3. Select the JSON file — you'll be taken to the edit form to review and adjust the configuration before saving
|
||||
4. Click **Create Agent** to finalize the import
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
All agent-related settings can be configured via environment variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `LOCALAI_DISABLE_AGENTS` | `false` | Disable the agent pool feature entirely |
|
||||
| `LOCALAI_AGENT_POOL_API_URL` | _(self-referencing)_ | Default API URL for agents. By default, agents call back into LocalAI's own API (`http://127.0.0.1:<port>`). Set this to point agents to an external LLM provider. |
|
||||
| `LOCALAI_AGENT_POOL_API_KEY` | _(LocalAI key)_ | Default API key for agents. Defaults to the first LocalAI API key. Set this when using an external provider. |
|
||||
| `LOCALAI_AGENT_POOL_DEFAULT_MODEL` | _(empty)_ | Default LLM model for new agents |
|
||||
| `LOCALAI_AGENT_POOL_MULTIMODAL_MODEL` | _(empty)_ | Default multimodal (vision) model for agents |
|
||||
| `LOCALAI_AGENT_POOL_TRANSCRIPTION_MODEL` | _(empty)_ | Default transcription (speech-to-text) model for agents |
|
||||
| `LOCALAI_AGENT_POOL_TRANSCRIPTION_LANGUAGE` | _(empty)_ | Default transcription language for agents |
|
||||
| `LOCALAI_AGENT_POOL_TTS_MODEL` | _(empty)_ | Default TTS (text-to-speech) model for agents |
|
||||
| `LOCALAI_AGENT_POOL_STATE_DIR` | _(config dir)_ | Directory for persisting agent state |
|
||||
| `LOCALAI_AGENT_POOL_TIMEOUT` | `5m` | Default timeout for agent operations |
|
||||
| `LOCALAI_AGENT_POOL_ENABLE_SKILLS` | `false` | Enable the skills service |
|
||||
| `LOCALAI_AGENT_POOL_VECTOR_ENGINE` | `chromem` | Vector engine for knowledge base (`chromem` or `postgres`) |
|
||||
| `LOCALAI_AGENT_POOL_EMBEDDING_MODEL` | `granite-embedding-107m-multilingual` | Embedding model for knowledge base |
|
||||
| `LOCALAI_AGENT_POOL_CUSTOM_ACTIONS_DIR` | _(empty)_ | Directory for custom action plugins |
|
||||
| `LOCALAI_AGENT_POOL_DATABASE_URL` | _(empty)_ | PostgreSQL connection string for collections (required when vector engine is `postgres`) |
|
||||
| `LOCALAI_AGENT_POOL_MAX_CHUNKING_SIZE` | `400` | Maximum chunk size for document ingestion |
|
||||
| `LOCALAI_AGENT_POOL_CHUNK_OVERLAP` | `0` | Overlap between document chunks |
|
||||
| `LOCALAI_AGENT_POOL_ENABLE_LOGS` | `false` | Enable detailed agent logging |
|
||||
| `LOCALAI_AGENT_POOL_COLLECTION_DB_PATH` | _(empty)_ | Custom path for the collections database |
|
||||
| `LOCALAI_AGENT_HUB_URL` | `https://agenthub.localai.io` | URL for the Agent Hub (shown in the UI) |
|
||||
|
||||
### Knowledge Base Storage
|
||||
|
||||
By default, the knowledge base uses **chromem** — an in-process vector store that requires no external dependencies. For production deployments with larger knowledge bases, you can switch to **PostgreSQL** with pgvector support:
|
||||
|
||||
```bash
|
||||
LOCALAI_AGENT_POOL_VECTOR_ENGINE=postgres
|
||||
LOCALAI_AGENT_POOL_DATABASE_URL=postgresql://localrecall:localrecall@postgres:5432/localrecall?sslmode=disable
|
||||
```
|
||||
|
||||
The PostgreSQL image `quay.io/mudler/localrecall:v0.5.2-postgresql` is pre-configured with pgvector and ready to use.
|
||||
|
||||
### Docker Compose Example
|
||||
|
||||
Basic setup with in-memory vector store:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
localai:
|
||||
image: localai/localai:latest
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
- MODELS_PATH=/models
|
||||
- LOCALAI_AGENT_POOL_DEFAULT_MODEL=hermes-3-llama3.1-8b
|
||||
- LOCALAI_AGENT_POOL_EMBEDDING_MODEL=granite-embedding-107m-multilingual
|
||||
- LOCALAI_AGENT_POOL_ENABLE_SKILLS=true
|
||||
- LOCALAI_AGENT_POOL_ENABLE_LOGS=true
|
||||
volumes:
|
||||
- models:/models
|
||||
- localai_config:/etc/localai
|
||||
volumes:
|
||||
models:
|
||||
localai_config:
|
||||
```
|
||||
|
||||
Setup with PostgreSQL for persistent knowledge base:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
localai:
|
||||
image: localai/localai:latest
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
- MODELS_PATH=/models
|
||||
- LOCALAI_AGENT_POOL_DEFAULT_MODEL=hermes-3-llama3.1-8b
|
||||
- LOCALAI_AGENT_POOL_EMBEDDING_MODEL=granite-embedding-107m-multilingual
|
||||
- LOCALAI_AGENT_POOL_ENABLE_SKILLS=true
|
||||
- LOCALAI_AGENT_POOL_ENABLE_LOGS=true
|
||||
# PostgreSQL-backed knowledge base
|
||||
- LOCALAI_AGENT_POOL_VECTOR_ENGINE=postgres
|
||||
- LOCALAI_AGENT_POOL_DATABASE_URL=postgresql://localrecall:localrecall@postgres:5432/localrecall?sslmode=disable
|
||||
volumes:
|
||||
- models:/models
|
||||
- localai_config:/etc/localai
|
||||
|
||||
postgres:
|
||||
image: quay.io/mudler/localrecall:v0.5.2-postgresql
|
||||
environment:
|
||||
- POSTGRES_DB=localrecall
|
||||
- POSTGRES_USER=localrecall
|
||||
- POSTGRES_PASSWORD=localrecall
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U localrecall"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
models:
|
||||
localai_config:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
## Agent Configuration
|
||||
|
||||
Each agent has its own configuration that controls its behavior. Key settings include:
|
||||
|
||||
- **Name** — unique identifier for the agent
|
||||
- **Model** — the LLM model the agent uses for reasoning
|
||||
- **System Prompt** — defines the agent's personality and instructions
|
||||
- **Actions** — tools the agent can use (web search, code execution, etc.)
|
||||
- **Connectors** — external integrations (Slack, Discord, etc.)
|
||||
- **Knowledge Base** — collections of documents for RAG
|
||||
- **MCP Servers** — Model Context Protocol servers for additional tool access
|
||||
|
||||
The pool-level defaults (API URL, API key, models) can be set via environment variables. Individual agents can further override these in their configuration, allowing them to use different LLM providers (OpenAI, other LocalAI instances, etc.) on a per-agent basis.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
All agent endpoints are grouped under `/api/agents/`:
|
||||
|
||||
### Agent Management
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/api/agents` | List all agents with status |
|
||||
| `POST` | `/api/agents` | Create a new agent |
|
||||
| `GET` | `/api/agents/:name` | Get agent info |
|
||||
| `PUT` | `/api/agents/:name` | Update agent configuration |
|
||||
| `DELETE` | `/api/agents/:name` | Delete an agent |
|
||||
| `GET` | `/api/agents/:name/config` | Get agent configuration |
|
||||
| `PUT` | `/api/agents/:name/pause` | Pause an agent |
|
||||
| `PUT` | `/api/agents/:name/resume` | Resume a paused agent |
|
||||
| `GET` | `/api/agents/:name/status` | Get agent status and observables |
|
||||
| `POST` | `/api/agents/:name/chat` | Send a message to an agent |
|
||||
| `GET` | `/api/agents/:name/sse` | SSE stream for real-time agent events |
|
||||
| `GET` | `/api/agents/:name/export` | Export agent configuration as JSON |
|
||||
| `POST` | `/api/agents/import` | Import an agent from JSON |
|
||||
| `GET` | `/api/agents/config/metadata` | Get dynamic config form metadata |
|
||||
|
||||
### Skills
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/api/agents/skills` | List all skills |
|
||||
| `POST` | `/api/agents/skills` | Create a new skill |
|
||||
| `GET` | `/api/agents/skills/:name` | Get a skill |
|
||||
| `PUT` | `/api/agents/skills/:name` | Update a skill |
|
||||
| `DELETE` | `/api/agents/skills/:name` | Delete a skill |
|
||||
| `GET` | `/api/agents/skills/search` | Search skills |
|
||||
| `GET` | `/api/agents/skills/export/*` | Export a skill |
|
||||
| `POST` | `/api/agents/skills/import` | Import a skill |
|
||||
|
||||
### Collections (Knowledge Base)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/api/agents/collections` | List collections |
|
||||
| `POST` | `/api/agents/collections` | Create a collection |
|
||||
| `POST` | `/api/agents/collections/:name/upload` | Upload a document |
|
||||
| `GET` | `/api/agents/collections/:name/entries` | List entries |
|
||||
| `POST` | `/api/agents/collections/:name/search` | Search a collection |
|
||||
| `POST` | `/api/agents/collections/:name/reset` | Reset a collection |
|
||||
|
||||
### Actions
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/api/agents/actions` | List available actions |
|
||||
| `POST` | `/api/agents/actions/:name/definition` | Get action definition |
|
||||
| `POST` | `/api/agents/actions/:name/run` | Execute an action |
|
||||
|
||||
## Using Agents via the Responses API
|
||||
|
||||
Agents can be used programmatically via the standard `/v1/responses` endpoint (OpenAI Responses API). Simply use the agent name as the `model` field:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/v1/responses \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "my-agent",
|
||||
"input": "What is the weather today?"
|
||||
}'
|
||||
```
|
||||
|
||||
This returns a standard Responses API response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "resp_...",
|
||||
"object": "response",
|
||||
"status": "completed",
|
||||
"model": "my-agent",
|
||||
"output": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": "The agent's response..."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can also send structured message arrays as input:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/v1/responses \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "my-agent",
|
||||
"input": [
|
||||
{"role": "user", "content": "Summarize the latest news about AI"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
When the model name matches an agent, the request is routed to the agent pool. If no agent matches, it falls through to the normal model-based inference pipeline.
|
||||
|
||||
## Chat with SSE Streaming
|
||||
|
||||
For real-time streaming responses, use the chat endpoint with SSE:
|
||||
|
||||
Send a message to an agent:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/agents/my-agent/chat \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message": "What is the weather today?"}'
|
||||
```
|
||||
|
||||
Listen to real-time events via SSE:
|
||||
|
||||
```bash
|
||||
curl -N http://localhost:8080/api/agents/my-agent/sse
|
||||
```
|
||||
|
||||
The SSE stream emits the following event types:
|
||||
|
||||
- `json_message` — agent/user messages
|
||||
- `json_message_status` — processing status updates (`processing` / `completed`)
|
||||
- `status` — system messages (reasoning steps, action results)
|
||||
- `json_error` — error notifications
|
||||
|
||||
## Architecture
|
||||
|
||||
Agents run in-process within LocalAI. By default, each agent calls back into LocalAI's own API (`http://127.0.0.1:<port>/v1/chat/completions`) for LLM inference. This means:
|
||||
|
||||
- No external dependencies — everything runs in a single binary
|
||||
- Agents use the same models loaded in LocalAI
|
||||
- Per-agent overrides allow pointing individual agents to external providers
|
||||
- Agent state is persisted to disk and restored on restart
|
||||
|
||||
```
|
||||
User → POST /api/agents/:name/chat → LocalAI
|
||||
→ AgentPool → Agent reasoning loop
|
||||
→ POST /v1/chat/completions (self-referencing)
|
||||
→ LocalAI model inference → response
|
||||
→ SSE events → GET /api/agents/:name/sse → UI
|
||||
```
|
||||
@@ -82,6 +82,20 @@ Manage model and backend galleries:
|
||||
- **Autoload Galleries**: Automatically load model galleries on startup
|
||||
- **Autoload Backend Galleries**: Automatically load backend galleries on startup
|
||||
|
||||
### Agent Pool Settings
|
||||
|
||||
Configure the built-in agent platform (see [Agents]({{%relref "features/agents" %}}) for full documentation):
|
||||
|
||||
- **Agent Pool Enabled**: Enable or disable the agent pool feature
|
||||
- **Default Model**: Default LLM model for new agents
|
||||
- **Embedding Model**: Model used for knowledge base embeddings (default: `granite-embedding-107m-multilingual`)
|
||||
- **Max Chunking Size**: Maximum chunk size for document ingestion (default: `400`)
|
||||
- **Chunk Overlap**: Overlap between document chunks (default: `0`)
|
||||
- **Enable Logs**: Enable detailed agent logging
|
||||
- **Collection DB Path**: Custom path for the collections database
|
||||
|
||||
> **Note:** Most agent pool settings require a restart to take effect.
|
||||
|
||||
## Configuration Persistence
|
||||
|
||||
All settings are automatically saved to `runtime_settings.json` in the `LOCALAI_CONFIG_DIR` directory (default: `BASEPATH/configuration`). This file is watched for changes, so modifications made directly to the file will also be applied at runtime.
|
||||
|
||||
99
go.mod
99
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/mudler/LocalAI
|
||||
|
||||
go 1.25.6
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2
|
||||
@@ -31,7 +31,7 @@ require (
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.0
|
||||
github.com/mudler/cogito v0.9.1
|
||||
github.com/mudler/cogito v0.9.3-0.20260306202429-e073d115bd04
|
||||
github.com/mudler/edgevpn v0.31.1
|
||||
github.com/mudler/go-processmanager v0.1.0
|
||||
github.com/mudler/memory v0.0.0-20251216220809-d1256471a6c2
|
||||
@@ -59,23 +59,108 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.41.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0
|
||||
google.golang.org/grpc v1.79.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
oras.land/oras-go/v2 v2.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/JohannesKaufmann/dom v0.2.0 // indirect
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||
github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/antchfx/htmlquery v1.3.4 // indirect
|
||||
github.com/antchfx/xmlquery v1.4.4 // indirect
|
||||
github.com/antchfx/xpath v1.3.4 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.22.0 // indirect
|
||||
github.com/blevesearch/bleve/v2 v2.5.7 // indirect
|
||||
github.com/blevesearch/bleve_index_api v1.2.11 // indirect
|
||||
github.com/blevesearch/geo v0.2.4 // indirect
|
||||
github.com/blevesearch/go-faiss v1.0.26 // indirect
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
||||
github.com/blevesearch/gtreap v0.1.1 // indirect
|
||||
github.com/blevesearch/mmap-go v1.0.4 // indirect
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 // indirect
|
||||
github.com/blevesearch/segment v0.9.1 // indirect
|
||||
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
|
||||
github.com/blevesearch/vellum v1.1.0 // indirect
|
||||
github.com/blevesearch/zapx/v11 v11.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v12 v12.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v13 v13.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v16 v16.2.8 // indirect
|
||||
github.com/bwmarrin/discordgo v0.29.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.5.1 // indirect
|
||||
github.com/dslipak/pdf v0.0.2 // indirect
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.5 // indirect
|
||||
github.com/emersion/go-message v0.18.2 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||
github.com/emersion/go-smtp v0.24.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-git/go-git/v5 v5.16.4 // indirect
|
||||
github.com/go-telegram/bot v1.17.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gocolly/colly v1.2.0 // indirect
|
||||
github.com/gofiber/fiber/v2 v2.52.9 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b // indirect
|
||||
github.com/google/go-github/v69 v69.2.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.8.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jung-kurt/gofpdf v1.16.2 // indirect
|
||||
github.com/kennygrant/sanitize v1.2.4 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/mudler/LocalAGI v0.0.0-20260306154948-5a27c471ca78
|
||||
github.com/mudler/localrecall v0.5.4 // indirect
|
||||
github.com/mudler/skillserver v0.0.5-0.20260221145827-0639a82c8f49
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/oxffaa/gopher-parse-sitemap v0.0.0-20191021113419-005d2eb1def4 // indirect
|
||||
github.com/philippgille/chromem-go v0.7.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/rs/zerolog v1.31.0 // indirect
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||
github.com/segmentio/asm v1.1.3 // indirect
|
||||
github.com/segmentio/encoding v0.5.3 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/slack-go/slack v0.17.3 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/swaggo/files/v2 v2.0.2 // indirect
|
||||
github.com/temoto/robotstxt v1.1.2 // indirect
|
||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/traefik/yaegi v0.16.1 // indirect
|
||||
github.com/valyala/fasthttp v1.68.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
go.etcd.io/bbolt v1.4.0 // indirect
|
||||
go.mau.fi/util v0.3.0 // indirect
|
||||
go.starlark.net v0.0.0-20250417143717-f57e51f710eb // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
|
||||
maunium.net/go/maulogger/v2 v2.4.1 // indirect
|
||||
maunium.net/go/mautrix v0.17.0 // indirect
|
||||
mvdan.cc/xurls/v2 v2.6.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -187,7 +272,7 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/docker/cli v29.2.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
@@ -207,7 +292,7 @@ require (
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
@@ -285,7 +370,7 @@ require (
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.2 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
|
||||
github.com/pkoukk/tiktoken-go v0.1.7 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/polydawn/refmt v0.89.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
@@ -312,7 +397,7 @@ require (
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
github.com/yuin/goldmark v1.7.13 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
|
||||
265
go.sum
265
go.sum
@@ -20,6 +20,10 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg6
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ=
|
||||
github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0 h1:C0/TerKdQX9Y9pbYi1EsLr5LDNANsqunyI/btpyfCg8=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0/go.mod h1:OLaKh+giepO8j7teevrNwiy/fwf8LXgoc9g7rwaE1jk=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
@@ -28,10 +32,17 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ=
|
||||
github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||
@@ -43,9 +54,22 @@ github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW5
|
||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/antchfx/htmlquery v1.3.4 h1:Isd0srPkni2iNTWCwVj/72t7uCphFeor5Q8nCzj1jdQ=
|
||||
github.com/antchfx/htmlquery v1.3.4/go.mod h1:K9os0BwIEmLAvTqaNSua8tXLWRWZpocZIH73OzWQbwM=
|
||||
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
|
||||
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
|
||||
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4=
|
||||
github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY=
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
@@ -57,8 +81,50 @@ github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
|
||||
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8=
|
||||
github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA=
|
||||
github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s=
|
||||
github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
|
||||
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
|
||||
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
|
||||
github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI=
|
||||
github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
||||
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
|
||||
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
|
||||
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
|
||||
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc=
|
||||
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
|
||||
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
|
||||
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
|
||||
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
|
||||
github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w=
|
||||
github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y=
|
||||
github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=
|
||||
github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
|
||||
github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=
|
||||
github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
|
||||
github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks=
|
||||
github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
|
||||
github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0=
|
||||
github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
|
||||
github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
|
||||
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
|
||||
github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI=
|
||||
github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14=
|
||||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
||||
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/c-robinson/iplib v1.0.8 h1:exDRViDyL9UBLcfmlxxkY5odWX5092nPsQIykHXhIn4=
|
||||
github.com/c-robinson/iplib v1.0.8/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
@@ -85,6 +151,8 @@ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNE
|
||||
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
|
||||
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
|
||||
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
|
||||
@@ -103,6 +171,7 @@ github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7np
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
@@ -112,6 +181,8 @@ github.com/creachadair/otp v0.5.0 h1:q3Th7CXm2zlmCdBjw5tEPFOj4oWJMnVL5HXlq0sNKS0
|
||||
github.com/creachadair/otp v0.5.0/go.mod h1:0kceI87EnYFNYSTL121goJVAnk3eJhaed9H0nMuJUkA=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1 h1:eYgfMq5yryL4fbWfkLpFFy2ukSELzaJOTaUTuh+oF48=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -126,8 +197,8 @@ github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4m
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
|
||||
@@ -142,18 +213,34 @@ github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pM
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dslipak/pdf v0.0.2 h1:djAvcM5neg9Ush+zR6QXB+VMJzR6TdnX766HPIg1JmI=
|
||||
github.com/dslipak/pdf v0.0.2/go.mod h1:2L3SnkI9cQwnAS9gfPz2iUoLC0rUZwbucpbKi5R1mUo=
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.5 h1:H3858DNmBuXyMK1++YrQIRdpKE1MwBc+ywBtg3n+0wA=
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.5/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk=
|
||||
github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
|
||||
github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
|
||||
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10 h1:mL93ADvYMOeT15DcGtK9AaFFc+RcWcy6kQBC6yS/5f4=
|
||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10/go.mod h1:BdpHs6imOtzE5KorbUtKa6bZ0ZBh1yFcrTTAL8FwDKY=
|
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
@@ -187,6 +274,8 @@ github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6O
|
||||
github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE=
|
||||
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4=
|
||||
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
||||
github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA=
|
||||
@@ -194,6 +283,14 @@ github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38r
|
||||
github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g=
|
||||
github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
|
||||
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
|
||||
@@ -218,6 +315,10 @@ github.com/go-skynet/go-llama.cpp v0.0.0-20240314183750-6a8041ef6b46 h1:lALhXzDk
|
||||
github.com/go-skynet/go-llama.cpp v0.0.0-20240314183750-6a8041ef6b46/go.mod h1:iub0ugfTnflE3rcIuqV2pQSo15nEw3GLW/utm5gyERo=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-telegram/bot v1.17.0 h1:Hs0kGxSj97QFqOQP0zxduY/4tSx8QDzvNI9uVRS+zmY=
|
||||
github.com/go-telegram/bot v1.17.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
|
||||
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
|
||||
github.com/go-text/typesetting v0.3.3 h1:ihGNJU9KzdK2QRDy1Bm7FT5RFQoYb+3n3EIhI/4eaQc=
|
||||
@@ -225,10 +326,17 @@ github.com/go-text/typesetting v0.3.3/go.mod h1:vIRUT25mLQaSh4C8H/lIsKppQz/Gdb8P
|
||||
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8 h1:4KCscI9qYWMGTuz6BpJtbUSRzcBrUSSE0ENMJbNSrFs=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
|
||||
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
|
||||
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
@@ -238,8 +346,9 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
@@ -253,11 +362,15 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
@@ -266,14 +379,20 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-containerregistry v0.21.1 h1:sOt/o9BS2b87FnR7wxXPvRKU1XVJn2QCwOS5g8zQXlc=
|
||||
github.com/google/go-containerregistry v0.21.1/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE=
|
||||
github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
@@ -294,6 +413,7 @@ github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRid
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gpustack/gguf-parser-go v0.24.0 h1:tdJceXYp9e5RhE9RwVYIuUpir72Jz2D68NEtDXkKCKc=
|
||||
@@ -342,12 +462,22 @@ github.com/ipfs/go-test v0.2.1 h1:/D/a8xZ2JzkYqcVcV/7HYlCnc7bv/pKHQiX5TdClkPE=
|
||||
github.com/ipfs/go-test v0.2.1/go.mod h1:dzu+KB9cmWjuJnXFDYJwC25T3j1GcN57byN+ixmK39M=
|
||||
github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E=
|
||||
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/jaypipes/ghw v0.23.0 h1:WOL4hpLcIu1kIm+z5Oz19Tk1HNw/Sncrx/6GS8O0Kl0=
|
||||
github.com/jaypipes/ghw v0.23.0/go.mod h1:fUNUjMZ0cjahKo+/u+32m9FutIx53Nkbi0Ti0m7j5HY=
|
||||
github.com/jaypipes/pcidb v1.1.1 h1:QmPhpsbmmnCwZmHeYAATxEaoRuiMAJusKYkUncMC0ro=
|
||||
github.com/jaypipes/pcidb v1.1.1/go.mod h1:x27LT2krrUgjf875KxQXKB0Ha/YXLdZRVmw6hH0G7g8=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
|
||||
@@ -368,6 +498,13 @@ github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe9
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
|
||||
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
|
||||
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
|
||||
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
@@ -444,10 +581,14 @@ github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU=
|
||||
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
|
||||
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
@@ -511,16 +652,26 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P
|
||||
github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/mudler/cogito v0.9.1 h1:6y7VPHSS+Q+v4slV42XcjykN5wip4N7C/rXTwWPBVFM=
|
||||
github.com/mudler/cogito v0.9.1/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
|
||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
github.com/mudler/LocalAGI v0.0.0-20260306154948-5a27c471ca78 h1:B3FgipRORpDtDvNlCC/w4N6PPwIyn7M/mzeRiq0EV4o=
|
||||
github.com/mudler/LocalAGI v0.0.0-20260306154948-5a27c471ca78/go.mod h1:e/00in01SHCpzUD/UyJMopn7P+vJMjsk6qkxZC1qPW0=
|
||||
github.com/mudler/cogito v0.9.2 h1:KbzNpuJ782njeBKfg3q7kLIBHTCFi9DgXhPTXnZqu1Y=
|
||||
github.com/mudler/cogito v0.9.2/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
|
||||
github.com/mudler/cogito v0.9.3-0.20260306202429-e073d115bd04 h1:33Lqv8VBaV/AoaaVtZ5+Bcig4T9fvj0dQmKFCon5Xxo=
|
||||
github.com/mudler/cogito v0.9.3-0.20260306202429-e073d115bd04/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
|
||||
github.com/mudler/edgevpn v0.31.1 h1:7qegiDWd0kAg6ljhNHxqvp8hbo/6BbzSdbb7/2WZfiY=
|
||||
github.com/mudler/edgevpn v0.31.1/go.mod h1:ftV5B0nKFzm4R8vR80UYnCb2nf7lxCRgAALxUEEgCf8=
|
||||
github.com/mudler/go-piper v0.0.0-20241023091659-2494246fd9fc h1:RxwneJl1VgvikiX28EkpdAyL4yQVnJMrbquKospjHyA=
|
||||
github.com/mudler/go-piper v0.0.0-20241023091659-2494246fd9fc/go.mod h1:O7SwdSWMilAWhBZMK9N9Y/oBDyMMzshE3ju8Xkexwig=
|
||||
github.com/mudler/go-processmanager v0.1.0 h1:fcSKgF9U/a1Z7KofAFeZnke5YseadCI5GqL9oT0LS3E=
|
||||
github.com/mudler/go-processmanager v0.1.0/go.mod h1:h6kmHUZeafr+k5hRYpGLMzJFH4hItHffgpRo2QIkP+o=
|
||||
github.com/mudler/localrecall v0.5.4 h1:hVPGHRDBOkGUJYL6Zm37sG8uoZObc8jtIJlDY/+NVb4=
|
||||
github.com/mudler/localrecall v0.5.4/go.mod h1:TZVXQI840MqjDtilBLc7kfmnctK4oNf1IR+cE68zno8=
|
||||
github.com/mudler/memory v0.0.0-20251216220809-d1256471a6c2 h1:+WHsL/j6EWOMUiMVIOJNKOwSKiQt/qDPc9fePCf87fA=
|
||||
github.com/mudler/memory v0.0.0-20251216220809-d1256471a6c2/go.mod h1:EA8Ashhd56o32qN7ouPKFSRUs/Z+LrRCF4v6R2Oarm8=
|
||||
github.com/mudler/skillserver v0.0.5-0.20260221145827-0639a82c8f49 h1:dAF1ALXqqapRZo80x56BIBBcPrPbRNerbd66rdyO8J4=
|
||||
github.com/mudler/skillserver v0.0.5-0.20260221145827-0639a82c8f49/go.mod h1:z3yFhcL9bSykmmh6xgGu0hyoItd4CnxgtWMEWw8uFJU=
|
||||
github.com/mudler/water v0.0.0-20250808092830-dd90dcf09025 h1:WFLP5FHInarYGXi6B/Ze204x7Xy6q/I4nCZnWEyPHK0=
|
||||
github.com/mudler/water v0.0.0-20250808092830-dd90dcf09025/go.mod h1:QuIFdRstyGJt+MTTkWY+mtD7U6xwjOR6SwKUjmLZtR4=
|
||||
github.com/mudler/xlog v0.0.5 h1:2unBuVC5rNGhCC86UaA94TElWFml80NL5XLK+kAmNuU=
|
||||
@@ -563,6 +714,8 @@ github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7
|
||||
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
|
||||
@@ -584,12 +737,17 @@ github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
|
||||
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
||||
github.com/otiai10/openaigo v1.7.0 h1:AOQcOjRRM57ABvz+aI2oJA/Qsz1AydKbdZAlGiKyCqg=
|
||||
github.com/otiai10/openaigo v1.7.0/go.mod h1:kIaXc3V+Xy5JLplcBxehVyGYDtufHp3PFPy04jOwOAI=
|
||||
github.com/oxffaa/gopher-parse-sitemap v0.0.0-20191021113419-005d2eb1def4 h1:2vmb32OdDhjZf2ETGDlr9n8RYXx7c+jXPxMiPbwnA+8=
|
||||
github.com/oxffaa/gopher-parse-sitemap v0.0.0-20191021113419-005d2eb1def4/go.mod h1:2JQx4jDHmWrbABvpOayg/+OTU6ehN0IyK2EHzceXpJo=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
|
||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
|
||||
github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY=
|
||||
github.com/philippgille/chromem-go v0.7.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo=
|
||||
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM=
|
||||
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
@@ -634,13 +792,15 @@ github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
|
||||
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
|
||||
github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54=
|
||||
github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||
github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
|
||||
github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -678,21 +838,31 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
|
||||
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
|
||||
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=
|
||||
github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
|
||||
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
|
||||
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
|
||||
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
|
||||
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
|
||||
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
|
||||
@@ -726,8 +896,13 @@ github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
|
||||
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
|
||||
github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d h1:3VwvTjiRPA7cqtgOWddEL+JrcijMlXUmj99c/6YyZoY=
|
||||
github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d/go.mod h1:tAG61zBM1DYRaGIPloumExGvScf08oHuo0kFoOqdbT0=
|
||||
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
@@ -747,6 +922,8 @@ github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiY
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/streamer45/silero-vad-go v0.2.1 h1:Li1/tTC4H/3cyw6q4weX+U8GWwEL3lTekK/nYa1Cvuk=
|
||||
github.com/streamer45/silero-vad-go v0.2.1/go.mod h1:B+2FXs/5fZ6pzl6unUZYhZqkYdOB+3saBVzjOzdZnUs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -772,8 +949,12 @@ github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
|
||||
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
|
||||
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
|
||||
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
|
||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 h1:l/T7dYuJEQZOwVOpjIXr1180aM9PZL/d1MnMVIxefX4=
|
||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64/go.mod h1:Q1NAJOuRdQCqN/VIWdnaaEhV8LpeO2rtlBP7/iDJNII=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
@@ -791,6 +972,8 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/tmc/langchaingo v0.1.14 h1:o1qWBPigAIuFvrG6cjTFo0cZPFEZ47ZqpOYMjM15yZc=
|
||||
github.com/tmc/langchaingo v0.1.14/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378=
|
||||
github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
|
||||
github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
|
||||
@@ -798,6 +981,8 @@ github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0o
|
||||
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
|
||||
@@ -816,6 +1001,8 @@ github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h
|
||||
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
@@ -828,12 +1015,16 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
|
||||
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
|
||||
go.mau.fi/util v0.3.0 h1:Lt3lbRXP6ZBqTINK0EieRWor3zEwwwrDT14Z5N8RUCs=
|
||||
go.mau.fi/util v0.3.0/go.mod h1:9dGsBCCbZJstx16YgnVMVi3O2bOizELoKpugLD4FoGs=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
@@ -859,6 +1050,8 @@ go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa
|
||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
||||
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
||||
go.starlark.net v0.0.0-20250417143717-f57e51f710eb h1:zOg9DxxrorEmgGUr5UPdCEwKqiqG0MlZciuCuA3XiDE=
|
||||
go.starlark.net v0.0.0-20250417143717-f57e51f710eb/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
|
||||
@@ -892,15 +1085,22 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -915,6 +1115,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -934,12 +1137,18 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -959,6 +1168,10 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -968,10 +1181,12 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -980,6 +1195,7 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -988,9 +1204,14 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0=
|
||||
golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -999,7 +1220,11 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1007,10 +1232,14 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1034,6 +1263,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1055,6 +1286,8 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
@@ -1086,10 +1319,13 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
@@ -1098,11 +1334,14 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
@@ -1117,8 +1356,16 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 h1:eeH1AIcPvSc0Z25ThsYF+Xoqbn0CI/YnXVYoTLFdGQw=
|
||||
howett.net/plist v1.0.2-0.20250314012144-ee69052608d9/go.mod h1:fyFX5Hj5tP1Mpk8obqA9MZgXT416Q5711SDT7dQLTLk=
|
||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g=
|
||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:OxvTsCwKosqQ1q7B+8FwXqg4rKZ/UG9dUW+g/VL2xH4=
|
||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
|
||||
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
|
||||
maunium.net/go/mautrix v0.17.0 h1:scc1qlUbzPn+wc+3eAPquyD+3gZwwy/hBANBm+iGKK8=
|
||||
maunium.net/go/mautrix v0.17.0/go.mod h1:j+puTEQCEydlVxhJ/dQP5chfa26TdvBO7X6F3Ataav8=
|
||||
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
|
||||
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
|
||||
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
|
||||
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
|
||||
Reference in New Issue
Block a user