Compare commits

...

13 Commits

Author SHA1 Message Date
Richard Palethorpe
92688cf498 Update README.md
Add observability screenshot and bullet point. Also update strap line and descriptions at the top to try and describe the benefit of this software
2025-04-24 16:13:58 +01:00
Richard Palethorpe
ce997d2425 fix: Handle state on agent restart and update observables (#75)
Keep some agent start across restarts, such as the SSE manager and
observer. This allows restarts to be shown on the state page and also
allows avatars to be kept when reconfiguring the agent.

Also observable updates can happen out of order because SSE manager has
multiple workers. For now handle this in the client.

Finally fix an issue with the IRC client to make it disconnect and
handle being assigned a different nickname by the server.

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-04-23 15:29:06 +02:00
Ettore Di Giacinto
56cd0e05ca chore: better defaults for parallel jobs (#76)
* chore: better defaults for parallel jobs

Signed-off-by: mudler <mudler@localai.io>

* chore(tests): add timeout

---------

Signed-off-by: mudler <mudler@localai.io>
2025-04-23 00:12:44 +02:00
Ettore Di Giacinto
25bb3fb123 feat: allow the agent to perform things concurrently (#74)
* feat: allow the agent to perform things concurrently

Signed-off-by: mudler <mudler@localai.io>

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* collect errors

Signed-off-by: mudler <mudler@localai.io>

---------

Signed-off-by: mudler <mudler@localai.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-22 16:49:28 +02:00
dependabot[bot]
9e52438877 chore(deps-dev): bump vite from 6.3.1 to 6.3.2 in /webui/react-ui (#69)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.1 to 6.3.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-22 11:44:53 +02:00
Ettore Di Giacinto
c4618896cf chore: default to gemma-3-12b-it-qat (#60)
* chore: default to gemma-3-12b-it-qat

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix: simplify tests to run faster

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-04-22 11:44:42 +02:00
Ettore Di Giacinto
ee1667d51a feat: add history metadata of agent browser (#71)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-04-21 22:52:04 +02:00
dependabot[bot]
bafd26e92c chore(deps-dev): bump eslint-plugin-react-hooks in /webui/react-ui (#67)
Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 5.2.0 to 6.0.0.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-hooks
  dependency-version: 6.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-21 21:31:48 +02:00
dependabot[bot]
8ecc18f76f chore(deps-dev): bump react-router-dom in /webui/react-ui (#65)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.5.0 to 7.5.1.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.5.1/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.5.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-21 21:09:56 +02:00
dependabot[bot]
985f07a529 chore(deps): bump github.com/metoro-io/mcp-golang from 0.9.0 to 0.11.0 (#64)
Bumps [github.com/metoro-io/mcp-golang](https://github.com/metoro-io/mcp-golang) from 0.9.0 to 0.11.0.
- [Release notes](https://github.com/metoro-io/mcp-golang/releases)
- [Changelog](https://github.com/metoro-io/mcp-golang/blob/main/.goreleaser.yml)
- [Commits](https://github.com/metoro-io/mcp-golang/compare/v0.9.0...v0.11.0)

---
updated-dependencies:
- dependency-name: github.com/metoro-io/mcp-golang
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-21 21:09:39 +02:00
dependabot[bot]
8b2900c6d8 chore(deps): bump github.com/sashabaranov/go-openai (#63)
Bumps [github.com/sashabaranov/go-openai](https://github.com/sashabaranov/go-openai) from 1.38.1 to 1.38.2.
- [Release notes](https://github.com/sashabaranov/go-openai/releases)
- [Commits](https://github.com/sashabaranov/go-openai/compare/v1.38.1...v1.38.2)

---
updated-dependencies:
- dependency-name: github.com/sashabaranov/go-openai
  dependency-version: 1.38.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-21 21:09:16 +02:00
Ettore Di Giacinto
50e56fe22f feat(browseragent): add browser agent runner action (#55)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-04-18 22:42:17 +02:00
Richard Palethorpe
b5a12a1da6 feat(ui): Structured observability/status view (#40)
* refactor(ui): Make message status SSE name more specific

Signed-off-by: Richard Palethorpe <io@richiejp.com>

* feat(ui): Add structured observability events

Signed-off-by: Richard Palethorpe <io@richiejp.com>

---------

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-04-18 17:32:43 +02:00
30 changed files with 1167 additions and 218 deletions

View File

@@ -9,7 +9,7 @@ cleanup-tests:
docker compose down docker compose down
tests: prepare-tests tests: prepare-tests
LOCALAGI_MODEL="arcee-agent" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --fail-fast -v -r ./... LOCALAGI_MODEL="gemma-3-12b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --fail-fast -v -r ./...
run-nokb: run-nokb:
$(MAKE) run KBDISABLEINDEX=true $(MAKE) run KBDISABLEINDEX=true

View File

@@ -2,7 +2,7 @@
<img src="./webui/react-ui/public/logo_1.png" alt="LocalAGI Logo" width="220"/> <img src="./webui/react-ui/public/logo_1.png" alt="LocalAGI Logo" width="220"/>
</p> </p>
<h3 align="center"><em>Your AI. Your Hardware. Your Rules.</em></h3> <h3 align="center"><em>Your AI. Your Hardware. Your Rules</em></h3>
<div align="center"> <div align="center">
@@ -13,9 +13,9 @@
</div> </div>
We empower you building AI Agents that you can run locally, without coding. Create customizable AI assistants, automations, chat bots and agents that run 100% locally. No need for agentic Python libraries or cloud service keys, just bring your GPU (or even just CPU) and a web browser.
**LocalAGI** is a powerful, self-hostable AI Agent platform designed for maximum privacy and flexibility. A complete drop-in replacement for OpenAI's Responses APIs with advanced agentic capabilities. No clouds. No data leaks. Just pure local AI that works on consumer-grade hardware (CPU and GPU). **LocalAGI** is a powerful, self-hostable AI Agent platform that allows you to design AI automations without writing code. A complete drop-in replacement for OpenAI's Responses APIs with advanced agentic capabilities. No clouds. No data leaks. Just pure local AI that works on consumer-grade hardware (CPU and GPU).
## 🛡️ Take Back Your Privacy ## 🛡️ Take Back Your Privacy
@@ -37,6 +37,7 @@ LocalAGI ensures your data stays exactly where you want it—on your hardware. N
- 🖼 **Multimodal Support**: Ready for vision, text, and more. - 🖼 **Multimodal Support**: Ready for vision, text, and more.
- 🔧 **Extensible Custom Actions**: Easily script dynamic agent behaviors in Go (interpreted, no compilation!). - 🔧 **Extensible Custom Actions**: Easily script dynamic agent behaviors in Go (interpreted, no compilation!).
- 🛠 **Fully Customizable Models**: Use your own models or integrate seamlessly with [LocalAI](https://github.com/mudler/LocalAI). - 🛠 **Fully Customizable Models**: Use your own models or integrate seamlessly with [LocalAI](https://github.com/mudler/LocalAI).
- 📊 **Observability**: Monitor agent status and view detailed observable updates in real-time.
## 🛠️ Quickstart ## 🛠️ Quickstart
@@ -114,7 +115,7 @@ LocalAGI supports multiple hardware configurations through Docker Compose profil
- Supports text, multimodal, and image generation models - Supports text, multimodal, and image generation models
- Run with: `docker compose -f docker-compose.nvidia.yaml up` - Run with: `docker compose -f docker-compose.nvidia.yaml up`
- Default models: - Default models:
- Text: `arcee-agent` - Text: `gemma-3-12b-it-qat`
- Multimodal: `minicpm-v-2_6` - Multimodal: `minicpm-v-2_6`
- Image: `sd-1.5-ggml` - Image: `sd-1.5-ggml`
- Environment variables: - Environment variables:
@@ -130,7 +131,7 @@ LocalAGI supports multiple hardware configurations through Docker Compose profil
- Supports text, multimodal, and image generation models - Supports text, multimodal, and image generation models
- Run with: `docker compose -f docker-compose.intel.yaml up` - Run with: `docker compose -f docker-compose.intel.yaml up`
- Default models: - Default models:
- Text: `arcee-agent` - Text: `gemma-3-12b-it-qat`
- Multimodal: `minicpm-v-2_6` - Multimodal: `minicpm-v-2_6`
- Image: `sd-1.5-ggml` - Image: `sd-1.5-ggml`
- Environment variables: - Environment variables:
@@ -161,7 +162,7 @@ docker compose -f docker-compose.intel.yaml up
``` ```
If no models are specified, it will use the defaults: If no models are specified, it will use the defaults:
- Text model: `arcee-agent` - Text model: `gemma-3-12b-it-qat`
- Multimodal model: `minicpm-v-2_6` - Multimodal model: `minicpm-v-2_6`
- Image model: `sd-1.5-ggml` - Image model: `sd-1.5-ggml`
@@ -194,6 +195,8 @@ LocalAGI is part of the powerful Local family of privacy-focused AI tools:
![Web UI Dashboard](https://github.com/user-attachments/assets/a40194f9-af3a-461f-8b39-5f4612fbf221) ![Web UI Dashboard](https://github.com/user-attachments/assets/a40194f9-af3a-461f-8b39-5f4612fbf221)
![Web UI Agent Settings](https://github.com/user-attachments/assets/fb3c3e2a-cd53-4ca8-97aa-c5da51ff1f83) ![Web UI Agent Settings](https://github.com/user-attachments/assets/fb3c3e2a-cd53-4ca8-97aa-c5da51ff1f83)
![Web UI Create Group](https://github.com/user-attachments/assets/102189a2-0fba-4a1e-b0cb-f99268ef8062) ![Web UI Create Group](https://github.com/user-attachments/assets/102189a2-0fba-4a1e-b0cb-f99268ef8062)
![Web UI Agent Observability](https://github.com/user-attachments/assets/f7359048-9d28-4cf1-9151-1f5556ce9235)
### Connectors Ready-to-Go ### Connectors Ready-to-Go

View File

@@ -2,7 +2,6 @@ package action
import ( import (
"context" "context"
"fmt"
"github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai/jsonschema" "github.com/sashabaranov/go-openai/jsonschema"
@@ -16,24 +15,6 @@ func NewState() *StateAction {
type StateAction struct{} type StateAction struct{}
// State is the structure
// that is used to keep track of the current state
// and the Agent's short memory that it can update
// Besides a long term memory that is accessible by the agent (With vector database),
// And a context memory (that is always powered by a vector database),
// this memory is the shorter one that the LLM keeps across conversation and across its
// reasoning process's and life time.
// TODO: A special action is then used to let the LLM itself update its memory
// periodically during self-processing, and the same action is ALSO exposed
// during the conversation to let the user put for example, a new goal to the agent.
type AgentInternalState struct {
NowDoing string `json:"doing_now"`
DoingNext string `json:"doing_next"`
DoneHistory []string `json:"done_history"`
Memories []string `json:"memories"`
Goal string `json:"goal"`
}
func (a *StateAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) { func (a *StateAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{Result: "internal state has been updated"}, nil return types.ActionResult{Result: "internal state has been updated"}, nil
} }
@@ -76,23 +57,3 @@ func (a *StateAction) Definition() types.ActionDefinition {
}, },
} }
} }
const fmtT = `=====================
NowDoing: %s
DoingNext: %s
Your current goal is: %s
You have done: %+v
You have a short memory with: %+v
=====================
`
func (c AgentInternalState) String() string {
return fmt.Sprintf(
fmtT,
c.NowDoing,
c.DoingNext,
c.Goal,
c.DoneHistory,
c.Memories,
)
}

View File

@@ -22,7 +22,7 @@ type decisionResult struct {
// decision forces the agent to take one of the available actions // decision forces the agent to take one of the available actions
func (a *Agent) decision( func (a *Agent) decision(
ctx context.Context, job *types.Job,
conversation []openai.ChatCompletionMessage, conversation []openai.ChatCompletionMessage,
tools []openai.Tool, toolchoice string, maxRetries int) (*decisionResult, error) { tools []openai.Tool, toolchoice string, maxRetries int) (*decisionResult, error) {
@@ -35,31 +35,63 @@ func (a *Agent) decision(
} }
} }
decision := openai.ChatCompletionRequest{
Model: a.options.LLMAPI.Model,
Messages: conversation,
Tools: tools,
}
if choice != nil {
decision.ToolChoice = *choice
}
var obs *types.Observable
if job.Obs != nil {
obs = a.observer.NewObservable()
obs.Name = "decision"
obs.ParentID = job.Obs.ID
obs.Icon = "brain"
obs.Creation = &types.Creation{
ChatCompletionRequest: &decision,
}
a.observer.Update(*obs)
}
var lastErr error var lastErr error
for attempts := 0; attempts < maxRetries; attempts++ { for attempts := 0; attempts < maxRetries; attempts++ {
decision := openai.ChatCompletionRequest{ resp, err := a.client.CreateChatCompletion(job.GetContext(), decision)
Model: a.options.LLMAPI.Model,
Messages: conversation,
Tools: tools,
}
if choice != nil {
decision.ToolChoice = *choice
}
resp, err := a.client.CreateChatCompletion(ctx, decision)
if err != nil { if err != nil {
lastErr = err lastErr = err
xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", err) xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", err)
if obs != nil {
obs.Progress = append(obs.Progress, types.Progress{
Error: err.Error(),
})
a.observer.Update(*obs)
}
continue continue
} }
jsonResp, _ := json.Marshal(resp) jsonResp, _ := json.Marshal(resp)
xlog.Debug("Decision response", "response", string(jsonResp)) xlog.Debug("Decision response", "response", string(jsonResp))
if obs != nil {
obs.AddProgress(types.Progress{
ChatCompletionResponse: &resp,
})
}
if len(resp.Choices) != 1 { if len(resp.Choices) != 1 {
lastErr = fmt.Errorf("no choices: %d", len(resp.Choices)) lastErr = fmt.Errorf("no choices: %d", len(resp.Choices))
xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", lastErr) xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", lastErr)
if obs != nil {
obs.Progress[len(obs.Progress)-1].Error = lastErr.Error()
a.observer.Update(*obs)
}
continue continue
} }
@@ -68,6 +100,12 @@ func (a *Agent) decision(
if err := a.saveConversation(append(conversation, msg), "decision"); err != nil { if err := a.saveConversation(append(conversation, msg), "decision"); err != nil {
xlog.Error("Error saving conversation", "error", err) xlog.Error("Error saving conversation", "error", err)
} }
if obs != nil {
obs.MakeLastProgressCompletion()
a.observer.Update(*obs)
}
return &decisionResult{message: msg.Content}, nil return &decisionResult{message: msg.Content}, nil
} }
@@ -75,6 +113,12 @@ func (a *Agent) decision(
if err := params.Read(msg.ToolCalls[0].Function.Arguments); err != nil { if err := params.Read(msg.ToolCalls[0].Function.Arguments); err != nil {
lastErr = err lastErr = err
xlog.Warn("Attempt to parse action parameters failed", "attempt", attempts+1, "error", err) xlog.Warn("Attempt to parse action parameters failed", "attempt", attempts+1, "error", err)
if obs != nil {
obs.Progress[len(obs.Progress)-1].Error = lastErr.Error()
a.observer.Update(*obs)
}
continue continue
} }
@@ -82,6 +126,11 @@ func (a *Agent) decision(
xlog.Error("Error saving conversation", "error", err) xlog.Error("Error saving conversation", "error", err)
} }
if obs != nil {
obs.MakeLastProgressCompletion()
a.observer.Update(*obs)
}
return &decisionResult{actionParams: params, actioName: msg.ToolCalls[0].Function.Name, message: msg.Content}, nil return &decisionResult{actionParams: params, actioName: msg.ToolCalls[0].Function.Name, message: msg.Content}, nil
} }
@@ -173,7 +222,7 @@ func (m Messages) IsLastMessageFromRole(role string) bool {
return m[len(m)-1].Role == role return m[len(m)-1].Role == role
} }
func (a *Agent) generateParameters(ctx context.Context, pickTemplate string, act types.Action, c []openai.ChatCompletionMessage, reasoning string, maxAttempts int) (*decisionResult, error) { func (a *Agent) generateParameters(job *types.Job, pickTemplate string, act types.Action, c []openai.ChatCompletionMessage, reasoning string, maxAttempts int) (*decisionResult, error) {
stateHUD, err := renderTemplate(pickTemplate, a.prepareHUD(), a.availableActions(), reasoning) stateHUD, err := renderTemplate(pickTemplate, a.prepareHUD(), a.availableActions(), reasoning)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -201,7 +250,7 @@ func (a *Agent) generateParameters(ctx context.Context, pickTemplate string, act
var attemptErr error var attemptErr error
for attempts := 0; attempts < maxAttempts; attempts++ { for attempts := 0; attempts < maxAttempts; attempts++ {
result, attemptErr = a.decision(ctx, result, attemptErr = a.decision(job,
cc, cc,
a.availableActions().ToTools(), a.availableActions().ToTools(),
act.Definition().Name.String(), act.Definition().Name.String(),
@@ -263,7 +312,7 @@ func (a *Agent) handlePlanning(ctx context.Context, job *types.Job, chosenAction
subTaskAction := a.availableActions().Find(subtask.Action) subTaskAction := a.availableActions().Find(subtask.Action)
subTaskReasoning := fmt.Sprintf("%s Overall goal is: %s", subtask.Reasoning, planResult.Goal) subTaskReasoning := fmt.Sprintf("%s Overall goal is: %s", subtask.Reasoning, planResult.Goal)
params, err := a.generateParameters(ctx, pickTemplate, subTaskAction, conv, subTaskReasoning, maxRetries) params, err := a.generateParameters(job, pickTemplate, subTaskAction, conv, subTaskReasoning, maxRetries)
if err != nil { if err != nil {
xlog.Error("error generating action's parameters", "error", err) xlog.Error("error generating action's parameters", "error", err)
return conv, fmt.Errorf("error generating action's parameters: %w", err) return conv, fmt.Errorf("error generating action's parameters: %w", err)
@@ -293,7 +342,7 @@ func (a *Agent) handlePlanning(ctx context.Context, job *types.Job, chosenAction
break break
} }
result, err := a.runAction(ctx, subTaskAction, actionParams) result, err := a.runAction(job, subTaskAction, actionParams)
if err != nil { if err != nil {
xlog.Error("error running action", "error", err) xlog.Error("error running action", "error", err)
return conv, fmt.Errorf("error running action: %w", err) return conv, fmt.Errorf("error running action: %w", err)
@@ -378,7 +427,7 @@ func (a *Agent) prepareHUD() (promptHUD *PromptHUD) {
} }
// pickAction picks an action based on the conversation // pickAction picks an action based on the conversation
func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.ChatCompletionMessage, maxRetries int) (types.Action, types.ActionParams, string, error) { func (a *Agent) pickAction(job *types.Job, templ string, messages []openai.ChatCompletionMessage, maxRetries int) (types.Action, types.ActionParams, string, error) {
c := messages c := messages
xlog.Debug("[pickAction] picking action starts", "messages", messages) xlog.Debug("[pickAction] picking action starts", "messages", messages)
@@ -389,7 +438,7 @@ func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.
xlog.Debug("not forcing reasoning") xlog.Debug("not forcing reasoning")
// We also could avoid to use functions here and get just a reply from the LLM // We also could avoid to use functions here and get just a reply from the LLM
// and then use the reply to get the action // and then use the reply to get the action
thought, err := a.decision(ctx, thought, err := a.decision(job,
messages, messages,
a.availableActions().ToTools(), a.availableActions().ToTools(),
"", "",
@@ -431,7 +480,7 @@ func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.
}, c...) }, c...)
} }
thought, err := a.decision(ctx, thought, err := a.decision(job,
c, c,
types.Actions{action.NewReasoning()}.ToTools(), types.Actions{action.NewReasoning()}.ToTools(),
action.NewReasoning().Definition().Name.String(), maxRetries) action.NewReasoning().Definition().Name.String(), maxRetries)
@@ -467,7 +516,7 @@ func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.
// to avoid hallucinations // to avoid hallucinations
// Extract an action // Extract an action
params, err := a.decision(ctx, params, err := a.decision(job,
append(c, openai.ChatCompletionMessage{ append(c, openai.ChatCompletionMessage{
Role: "system", Role: "system",
Content: "Pick the relevant action given the following reasoning: " + originalReasoning, Content: "Pick the relevant action given the following reasoning: " + originalReasoning,

View File

@@ -2,6 +2,7 @@ package agent
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"sync" "sync"
@@ -30,7 +31,7 @@ type Agent struct {
jobQueue chan *types.Job jobQueue chan *types.Job
context *types.ActionContext context *types.ActionContext
currentState *action.AgentInternalState currentState *types.AgentInternalState
selfEvaluationInProgress bool selfEvaluationInProgress bool
pause bool pause bool
@@ -41,6 +42,8 @@ type Agent struct {
subscriberMutex sync.Mutex subscriberMutex sync.Mutex
newMessagesSubscribers []func(openai.ChatCompletionMessage) newMessagesSubscribers []func(openai.ChatCompletionMessage)
observer Observer
} }
type RAGDB interface { type RAGDB interface {
@@ -69,12 +72,17 @@ func New(opts ...Option) (*Agent, error) {
options: options, options: options,
client: client, client: client,
Character: options.character, Character: options.character,
currentState: &action.AgentInternalState{}, currentState: &types.AgentInternalState{},
context: types.NewActionContext(ctx, cancel), context: types.NewActionContext(ctx, cancel),
newConversations: make(chan openai.ChatCompletionMessage), newConversations: make(chan openai.ChatCompletionMessage),
newMessagesSubscribers: options.newConversationsSubscribers, newMessagesSubscribers: options.newConversationsSubscribers,
} }
// Initialize observer if provided
if options.observer != nil {
a.observer = options.observer
}
if a.options.statefile != "" { if a.options.statefile != "" {
if _, err := os.Stat(a.options.statefile); err == nil { if _, err := os.Stat(a.options.statefile); err == nil {
if err = a.LoadState(a.options.statefile); err != nil { if err = a.LoadState(a.options.statefile); err != nil {
@@ -146,6 +154,14 @@ func (a *Agent) Ask(opts ...types.JobOption) *types.JobResult {
xlog.Debug("Agent has finished being asked", "agent", a.Character.Name) xlog.Debug("Agent has finished being asked", "agent", a.Character.Name)
}() }()
if a.observer != nil {
obs := a.observer.NewObservable()
obs.Name = "job"
obs.Icon = "plug"
a.observer.Update(*obs)
opts = append(opts, types.WithObservable(obs))
}
return a.Execute(types.NewJob( return a.Execute(types.NewJob(
append( append(
opts, opts,
@@ -163,6 +179,20 @@ func (a *Agent) Execute(j *types.Job) *types.JobResult {
xlog.Debug("Agent has finished", "agent", a.Character.Name) xlog.Debug("Agent has finished", "agent", a.Character.Name)
}() }()
if j.Obs != nil {
j.Result.AddFinalizer(func(ccm []openai.ChatCompletionMessage) {
j.Obs.Completion = &types.Completion{
Conversation: ccm,
}
if j.Result.Error != nil {
j.Obs.Completion.Error = j.Result.Error.Error()
}
a.observer.Update(*j.Obs)
})
}
a.Enqueue(j) a.Enqueue(j)
return j.Result.WaitResult() return j.Result.WaitResult()
} }
@@ -237,34 +267,78 @@ func (a *Agent) Memory() RAGDB {
return a.options.ragdb return a.options.ragdb
} }
func (a *Agent) runAction(ctx context.Context, chosenAction types.Action, params types.ActionParams) (result types.ActionResult, err error) { func (a *Agent) runAction(job *types.Job, chosenAction types.Action, params types.ActionParams) (result types.ActionResult, err error) {
var obs *types.Observable
if job.Obs != nil {
obs = a.observer.NewObservable()
obs.Name = "action"
obs.Icon = "bolt"
obs.ParentID = job.Obs.ID
obs.Creation = &types.Creation{
FunctionDefinition: chosenAction.Definition().ToFunctionDefinition(),
FunctionParams: params,
}
a.observer.Update(*obs)
}
xlog.Info("[runAction] Running action", "action", chosenAction.Definition().Name, "agent", a.Character.Name, "params", params.String())
for _, act := range a.availableActions() { for _, act := range a.availableActions() {
if act.Definition().Name == chosenAction.Definition().Name { if act.Definition().Name == chosenAction.Definition().Name {
res, err := act.Run(ctx, params) res, err := act.Run(job.GetContext(), params)
if err != nil { if err != nil {
if obs != nil {
obs.Completion = &types.Completion{
Error: err.Error(),
}
}
return types.ActionResult{}, fmt.Errorf("error running action: %w", err) return types.ActionResult{}, fmt.Errorf("error running action: %w", err)
} }
if obs != nil {
obs.Progress = append(obs.Progress, types.Progress{
ActionResult: res.Result,
})
a.observer.Update(*obs)
}
result = res result = res
} }
} }
xlog.Info("[runAction] Running action", "action", chosenAction.Definition().Name, "agent", a.Character.Name, "params", params.String())
if chosenAction.Definition().Name.Is(action.StateActionName) { if chosenAction.Definition().Name.Is(action.StateActionName) {
// We need to store the result in the state // We need to store the result in the state
state := action.AgentInternalState{} state := types.AgentInternalState{}
err = params.Unmarshal(&state) err = params.Unmarshal(&state)
if err != nil { if err != nil {
return types.ActionResult{}, fmt.Errorf("error unmarshalling state of the agent: %w", err) werr := fmt.Errorf("error unmarshalling state of the agent: %w", err)
if obs != nil {
obs.Completion = &types.Completion{
Error: werr.Error(),
}
}
return types.ActionResult{}, werr
} }
// update the current state with the one we just got from the action // update the current state with the one we just got from the action
a.currentState = &state a.currentState = &state
if obs != nil {
obs.Progress = append(obs.Progress, types.Progress{
AgentState: &state,
})
a.observer.Update(*obs)
}
// update the state file // update the state file
if a.options.statefile != "" { if a.options.statefile != "" {
if err := a.SaveState(a.options.statefile); err != nil { if err := a.SaveState(a.options.statefile); err != nil {
if obs != nil {
obs.Completion = &types.Completion{
Error: err.Error(),
}
}
return types.ActionResult{}, err return types.ActionResult{}, err
} }
} }
@@ -272,6 +346,11 @@ func (a *Agent) runAction(ctx context.Context, chosenAction types.Action, params
xlog.Debug("[runAction] Action result", "action", chosenAction.Definition().Name, "params", params.String(), "result", result.Result) xlog.Debug("[runAction] Action result", "action", chosenAction.Definition().Name, "params", params.String(), "result", result.Result)
if obs != nil {
obs.MakeLastProgressCompletion()
a.observer.Update(*obs)
}
return result, nil return result, nil
} }
@@ -468,7 +547,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
chosenAction = *action chosenAction = *action
reasoning = reason reasoning = reason
if params == nil { if params == nil {
p, err := a.generateParameters(job.GetContext(), pickTemplate, chosenAction, conv, reasoning, maxRetries) p, err := a.generateParameters(job, pickTemplate, chosenAction, conv, reasoning, maxRetries)
if err != nil { if err != nil {
xlog.Error("Error generating parameters, trying again", "error", err) xlog.Error("Error generating parameters, trying again", "error", err)
// try again // try again
@@ -483,7 +562,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
job.ResetNextAction() job.ResetNextAction()
} else { } else {
var err error var err error
chosenAction, actionParams, reasoning, err = a.pickAction(job.GetContext(), pickTemplate, conv, maxRetries) chosenAction, actionParams, reasoning, err = a.pickAction(job, pickTemplate, conv, maxRetries)
if err != nil { if err != nil {
xlog.Error("Error picking action", "error", err) xlog.Error("Error picking action", "error", err)
job.Result.Finish(err) job.Result.Finish(err)
@@ -557,7 +636,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
"reasoning", reasoning, "reasoning", reasoning,
) )
params, err := a.generateParameters(job.GetContext(), pickTemplate, chosenAction, conv, reasoning, maxRetries) params, err := a.generateParameters(job, pickTemplate, chosenAction, conv, reasoning, maxRetries)
if err != nil { if err != nil {
xlog.Error("Error generating parameters, trying again", "error", err) xlog.Error("Error generating parameters, trying again", "error", err)
// try again // try again
@@ -652,7 +731,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
} }
if !chosenAction.Definition().Name.Is(action.PlanActionName) { if !chosenAction.Definition().Name.Is(action.PlanActionName) {
result, err := a.runAction(job.GetContext(), chosenAction, actionParams) result, err := a.runAction(job, chosenAction, actionParams)
if err != nil { if err != nil {
//job.Result.Finish(fmt.Errorf("error running action: %w", err)) //job.Result.Finish(fmt.Errorf("error running action: %w", err))
//return //return
@@ -677,7 +756,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
} }
// given the result, we can now re-evaluate the conversation // given the result, we can now re-evaluate the conversation
followingAction, followingParams, reasoning, err := a.pickAction(job.GetContext(), reEvaluationTemplate, conv, maxRetries) followingAction, followingParams, reasoning, err := a.pickAction(job, reEvaluationTemplate, conv, maxRetries)
if err != nil { if err != nil {
job.Result.Conversation = conv job.Result.Conversation = conv
job.Result.Finish(fmt.Errorf("error picking action: %w", err)) job.Result.Finish(fmt.Errorf("error picking action: %w", err))
@@ -911,7 +990,6 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
} }
func (a *Agent) Run() error { func (a *Agent) Run() error {
a.startNewConversationsConsumer() a.startNewConversationsConsumer()
xlog.Debug("Agent is now running", "agent", a.Character.Name) xlog.Debug("Agent is now running", "agent", a.Character.Name)
// The agent run does two things: // The agent run does two things:
@@ -926,32 +1004,68 @@ func (a *Agent) Run() error {
// Expose a REST API to interact with the agent to ask it things // Expose a REST API to interact with the agent to ask it things
//todoTimer := time.NewTicker(a.options.periodicRuns)
timer := time.NewTimer(a.options.periodicRuns) timer := time.NewTimer(a.options.periodicRuns)
// we fire the periodicalRunner only once.
go a.periodicalRunRunner(timer)
var errs []error
var muErr sync.Mutex
var wg sync.WaitGroup
parallelJobs := a.options.parallelJobs
if a.options.parallelJobs == 0 {
parallelJobs = 1
}
for i := 0; i < parallelJobs; i++ {
xlog.Debug("Starting agent worker", "worker", i)
wg.Add(1)
go func() {
e := a.run(timer)
muErr.Lock()
errs = append(errs, e)
muErr.Unlock()
wg.Done()
}()
}
wg.Wait()
return errors.Join(errs...)
}
func (a *Agent) run(timer *time.Timer) error {
for { for {
xlog.Debug("Agent is now waiting for a new job", "agent", a.Character.Name) xlog.Debug("Agent is now waiting for a new job", "agent", a.Character.Name)
select { select {
case job := <-a.jobQueue: case job := <-a.jobQueue:
a.loop(timer, job) if !timer.Stop() {
<-timer.C
}
xlog.Debug("Agent is consuming a job", "agent", a.Character.Name, "job", job)
a.consumeJob(job, UserRole)
timer.Reset(a.options.periodicRuns)
case <-a.context.Done(): case <-a.context.Done():
// Agent has been canceled, return error // Agent has been canceled, return error
xlog.Warn("Agent has been canceled", "agent", a.Character.Name) xlog.Warn("Agent has been canceled", "agent", a.Character.Name)
return ErrContextCanceled return ErrContextCanceled
}
}
}
func (a *Agent) periodicalRunRunner(timer *time.Timer) {
for {
select {
case <-a.context.Done():
// Agent has been canceled, return error
xlog.Warn("periodicalRunner has been canceled", "agent", a.Character.Name)
return
case <-timer.C: case <-timer.C:
a.periodicallyRun(timer) a.periodicallyRun(timer)
} }
} }
} }
func (a *Agent) loop(timer *time.Timer, job *types.Job) { func (a *Agent) Observer() Observer {
// Remember always to reset the timer - if we don't the agent will stop.. return a.observer
defer timer.Reset(a.options.periodicRuns)
// Consume the job and generate a response
// TODO: Give a short-term memory to the agent
// stop and drain the timer
if !timer.Stop() {
<-timer.C
}
xlog.Debug("Agent is consuming a job", "agent", a.Character.Name, "job", job)
a.consumeJob(job, UserRole)
} }

View File

@@ -226,7 +226,10 @@ var _ = Describe("Agent test", func() {
WithLLMAPIKey(apiKeyURL), WithLLMAPIKey(apiKeyURL),
WithTimeout("10m"), WithTimeout("10m"),
WithActions( WithActions(
actions.NewSearch(map[string]string{}), &TestAction{response: map[string]string{
"boston": testActionResult,
"milan": testActionResult2,
}},
), ),
EnablePlanning, EnablePlanning,
EnableForceReasoning, EnableForceReasoning,
@@ -238,18 +241,21 @@ var _ = Describe("Agent test", func() {
defer agent.Stop() defer agent.Stop()
result := agent.Ask( result := agent.Ask(
types.WithText("Thoroughly plan a trip to San Francisco from Venice, Italy; check flight times, visa requirements and whether electrical items are allowed in cabin luggage."), types.WithText("Use the plan tool to do two actions in sequence: search for the weather in boston and search for the weather in milan"),
) )
Expect(len(result.State)).To(BeNumerically(">", 1)) Expect(len(result.State)).To(BeNumerically(">", 1))
actionsExecuted := []string{} actionsExecuted := []string{}
actionResults := []string{}
for _, r := range result.State { for _, r := range result.State {
xlog.Info(r.Result) xlog.Info(r.Result)
actionsExecuted = append(actionsExecuted, r.Action.Definition().Name.String()) actionsExecuted = append(actionsExecuted, r.Action.Definition().Name.String())
actionResults = append(actionResults, r.ActionResult.Result)
} }
Expect(actionsExecuted).To(ContainElement("search_internet"), fmt.Sprint(result)) Expect(actionsExecuted).To(ContainElement("get_weather"), fmt.Sprint(result))
Expect(actionsExecuted).To(ContainElement("plan"), fmt.Sprint(result)) Expect(actionsExecuted).To(ContainElement("plan"), fmt.Sprint(result))
Expect(actionResults).To(ContainElement(testActionResult), fmt.Sprint(result))
Expect(actionResults).To(ContainElement(testActionResult2), fmt.Sprint(result))
}) })
It("Can initiate conversations", func() { It("Can initiate conversations", func() {

88
core/agent/observer.go Normal file
View File

@@ -0,0 +1,88 @@
package agent
import (
"encoding/json"
"sync"
"sync/atomic"
"github.com/mudler/LocalAGI/core/sse"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/xlog"
)
type Observer interface {
NewObservable() *types.Observable
Update(types.Observable)
History() []types.Observable
}
type SSEObserver struct {
agent string
maxID int32
manager sse.Manager
mutex sync.Mutex
history []types.Observable
historyLast int
}
func NewSSEObserver(agent string, manager sse.Manager) *SSEObserver {
return &SSEObserver{
agent: agent,
maxID: 1,
manager: manager,
history: make([]types.Observable, 100),
}
}
func (s *SSEObserver) NewObservable() *types.Observable {
id := atomic.AddInt32(&s.maxID, 1)
return &types.Observable{
ID: id - 1,
Agent: s.agent,
}
}
func (s *SSEObserver) Update(obs types.Observable) {
data, err := json.Marshal(obs)
if err != nil {
xlog.Error("Error marshaling observable", "error", err)
return
}
msg := sse.NewMessage(string(data)).WithEvent("observable_update")
s.manager.Send(msg)
s.mutex.Lock()
defer s.mutex.Unlock()
for i, o := range s.history {
if o.ID == obs.ID {
s.history[i] = obs
return
}
}
s.history[s.historyLast] = obs
s.historyLast += 1
if s.historyLast >= len(s.history) {
s.historyLast = 0
}
}
func (s *SSEObserver) History() []types.Observable {
h := make([]types.Observable, 0, 20)
s.mutex.Lock()
defer s.mutex.Unlock()
for _, obs := range s.history {
if obs.ID == 0 {
continue
}
h = append(h, obs)
}
return h
}

View File

@@ -53,6 +53,9 @@ type options struct {
mcpServers []MCPServer mcpServers []MCPServer
newConversationsSubscribers []func(openai.ChatCompletionMessage) newConversationsSubscribers []func(openai.ChatCompletionMessage)
observer Observer
parallelJobs int
} }
func (o *options) SeparatedMultimodalModel() bool { func (o *options) SeparatedMultimodalModel() bool {
@@ -61,6 +64,7 @@ func (o *options) SeparatedMultimodalModel() bool {
func defaultOptions() *options { func defaultOptions() *options {
return &options{ return &options{
parallelJobs: 1,
periodicRuns: 15 * time.Minute, periodicRuns: 15 * time.Minute,
LLMAPI: llmOptions{ LLMAPI: llmOptions{
APIURL: "http://localhost:8080", APIURL: "http://localhost:8080",
@@ -136,6 +140,13 @@ func EnableKnowledgeBaseWithResults(results int) Option {
} }
} }
func WithParallelJobs(jobs int) Option {
return func(o *options) error {
o.parallelJobs = jobs
return nil
}
}
func WithNewConversationSubscriber(sub func(openai.ChatCompletionMessage)) Option { func WithNewConversationSubscriber(sub func(openai.ChatCompletionMessage)) Option {
return func(o *options) error { return func(o *options) error {
o.newConversationsSubscribers = append(o.newConversationsSubscribers, sub) o.newConversationsSubscribers = append(o.newConversationsSubscribers, sub)
@@ -336,3 +347,10 @@ func WithActions(actions ...types.Action) Option {
return nil return nil
} }
} }
func WithObserver(observer Observer) Option {
return func(o *options) error {
o.observer = observer
return nil
}
}

View File

@@ -6,7 +6,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/mudler/LocalAGI/core/action" "github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai/jsonschema" "github.com/sashabaranov/go-openai/jsonschema"
) )
@@ -15,7 +15,7 @@ import (
// in the prompts // in the prompts
type PromptHUD struct { type PromptHUD struct {
Character Character `json:"character"` Character Character `json:"character"`
CurrentState action.AgentInternalState `json:"current_state"` CurrentState types.AgentInternalState `json:"current_state"`
PermanentGoal string `json:"permanent_goal"` PermanentGoal string `json:"permanent_goal"`
ShowCharacter bool `json:"show_character"` ShowCharacter bool `json:"show_character"`
} }
@@ -80,7 +80,7 @@ func Load(path string) (*Character, error) {
return &c, nil return &c, nil
} }
func (a *Agent) State() action.AgentInternalState { func (a *Agent) State() types.AgentInternalState {
return *a.currentState return *a.currentState
} }

View File

@@ -25,6 +25,7 @@ var _ = Describe("Agent test", func() {
agent, err = New( agent, err = New(
WithLLMAPIURL(apiURL), WithLLMAPIURL(apiURL),
WithModel(testModel), WithModel(testModel),
WithTimeout("10m"),
WithRandomIdentity(), WithRandomIdentity(),
) )
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())

View File

@@ -61,6 +61,7 @@ type AgentConfig struct {
SystemPrompt string `json:"system_prompt" form:"system_prompt"` SystemPrompt string `json:"system_prompt" form:"system_prompt"`
LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"` LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"`
SummaryLongTermMemory bool `json:"summary_long_term_memory" form:"summary_long_term_memory"` SummaryLongTermMemory bool `json:"summary_long_term_memory" form:"summary_long_term_memory"`
ParallelJobs int `json:"parallel_jobs" form:"parallel_jobs"`
} }
type AgentConfigMeta struct { type AgentConfigMeta struct {
@@ -260,6 +261,16 @@ func NewAgentConfigMeta(
Step: 1, Step: 1,
Tags: config.Tags{Section: "AdvancedSettings"}, Tags: config.Tags{Section: "AdvancedSettings"},
}, },
{
Name: "parallel_jobs",
Label: "Parallel Jobs",
Type: "number",
DefaultValue: 5,
Min: 1,
Step: 1,
HelpText: "Number of concurrent tasks that can run in parallel",
Tags: config.Tags{Section: "AdvancedSettings"},
},
}, },
MCPServers: []config.Field{ MCPServers: []config.Field{
{ {

View File

@@ -166,7 +166,56 @@ func (a *AgentPool) CreateAgent(name string, agentConfig *AgentConfig) error {
} }
}(a.pool[name]) }(a.pool[name])
return a.startAgentWithConfig(name, agentConfig) return a.startAgentWithConfig(name, agentConfig, nil)
}
func (a *AgentPool) RecreateAgent(name string, agentConfig *AgentConfig) error {
a.Lock()
defer a.Unlock()
oldAgent := a.agents[name]
var o *types.Observable
obs := oldAgent.Observer()
if obs != nil {
o = obs.NewObservable()
o.Name = "Restarting Agent"
o.Icon = "sync"
o.Creation = &types.Creation{}
obs.Update(*o)
}
stateFile, characterFile := a.stateFiles(name)
os.Remove(stateFile)
os.Remove(characterFile)
oldAgent.Stop()
a.pool[name] = *agentConfig
delete(a.agents, name)
if err := a.save(); err != nil {
if obs != nil {
o.Completion = &types.Completion{Error: err.Error()}
obs.Update(*o)
}
return err
}
if err := a.startAgentWithConfig(name, agentConfig, obs); err != nil {
if obs != nil {
o.Completion = &types.Completion{Error: err.Error()}
obs.Update(*o)
}
return err
}
if obs != nil {
o.Completion = &types.Completion{}
obs.Update(*o)
}
return nil
} }
func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agent AgentConfig) error { func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agent AgentConfig) error {
@@ -268,8 +317,13 @@ func (a *AgentPool) GetStatusHistory(name string) *Status {
return a.agentStatus[name] return a.agentStatus[name]
} }
func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error { func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs Observer) error {
manager := sse.NewManager(5) var manager sse.Manager
if m, ok := a.managers[name]; ok {
manager = m
} else {
manager = sse.NewManager(5)
}
ctx := context.Background() ctx := context.Background()
model := a.defaultModel model := a.defaultModel
multimodalModel := a.defaultMultimodalModel multimodalModel := a.defaultMultimodalModel
@@ -331,6 +385,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
// dynamicPrompts = append(dynamicPrompts, p.ToMap()) // dynamicPrompts = append(dynamicPrompts, p.ToMap())
// } // }
if obs == nil {
obs = NewSSEObserver(name, manager)
}
opts := []Option{ opts := []Option{
WithModel(model), WithModel(model),
WithLLMAPIURL(a.apiURL), WithLLMAPIURL(a.apiURL),
@@ -407,6 +465,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
c.AgentResultCallback()(state) c.AgentResultCallback()(state)
} }
}), }),
WithObserver(obs),
} }
if config.HUD { if config.HUD {
@@ -465,6 +524,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
opts = append(opts, WithLoopDetectionSteps(config.LoopDetectionSteps)) opts = append(opts, WithLoopDetectionSteps(config.LoopDetectionSteps))
} }
if config.ParallelJobs > 0 {
opts = append(opts, WithParallelJobs(config.ParallelJobs))
}
xlog.Info("Starting agent", "name", name, "config", config) xlog.Info("Starting agent", "name", name, "config", config)
agent, err := New(opts...) agent, err := New(opts...)
@@ -509,7 +572,7 @@ func (a *AgentPool) StartAll() error {
if a.agents[name] != nil { // Agent already started if a.agents[name] != nil { // Agent already started
continue continue
} }
if err := a.startAgentWithConfig(name, &config); err != nil { if err := a.startAgentWithConfig(name, &config, nil); err != nil {
xlog.Error("Failed to start agent", "name", name, "error", err) xlog.Error("Failed to start agent", "name", name, "error", err)
} }
} }
@@ -547,7 +610,7 @@ func (a *AgentPool) Start(name string) error {
return nil return nil
} }
if config, ok := a.pool[name]; ok { if config, ok := a.pool[name]; ok {
return a.startAgentWithConfig(name, &config) return a.startAgentWithConfig(name, &config, nil)
} }
return fmt.Errorf("agent %s not found", name) return fmt.Errorf("agent %s not found", name)

View File

@@ -27,6 +27,8 @@ type Job struct {
context context.Context context context.Context
cancel context.CancelFunc cancel context.CancelFunc
Obs *Observable
} }
type ActionRequest struct { type ActionRequest struct {
@@ -198,3 +200,9 @@ func (j *Job) Cancel() {
func (j *Job) GetContext() context.Context { func (j *Job) GetContext() context.Context {
return j.context return j.context
} }
func WithObservable(obs *Observable) JobOption {
return func(j *Job) {
j.Obs = obs
}
}

61
core/types/observable.go Normal file
View File

@@ -0,0 +1,61 @@
package types
import (
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/sashabaranov/go-openai"
)
type Creation struct {
ChatCompletionRequest *openai.ChatCompletionRequest `json:"chat_completion_request,omitempty"`
FunctionDefinition *openai.FunctionDefinition `json:"function_definition,omitempty"`
FunctionParams ActionParams `json:"function_params,omitempty"`
}
type Progress struct {
Error string `json:"error,omitempty"`
ChatCompletionResponse *openai.ChatCompletionResponse `json:"chat_completion_response,omitempty"`
ActionResult string `json:"action_result,omitempty"`
AgentState *AgentInternalState `json:"agent_state"`
}
type Completion struct {
Error string `json:"error,omitempty"`
ChatCompletionResponse *openai.ChatCompletionResponse `json:"chat_completion_response,omitempty"`
Conversation []openai.ChatCompletionMessage `json:"conversation,omitempty"`
ActionResult string `json:"action_result,omitempty"`
AgentState *AgentInternalState `json:"agent_state"`
}
type Observable struct {
ID int32 `json:"id"`
ParentID int32 `json:"parent_id,omitempty"`
Agent string `json:"agent"`
Name string `json:"name"`
Icon string `json:"icon"`
Creation *Creation `json:"creation,omitempty"`
Progress []Progress `json:"progress,omitempty"`
Completion *Completion `json:"completion,omitempty"`
}
func (o *Observable) AddProgress(p Progress) {
if o.Progress == nil {
o.Progress = make([]Progress, 0)
}
o.Progress = append(o.Progress, p)
}
func (o *Observable) MakeLastProgressCompletion() {
if len(o.Progress) == 0 {
xlog.Error("Observable completed without any progress", "id", o.ID, "name", o.Name)
return
}
p := o.Progress[len(o.Progress)-1]
o.Progress = o.Progress[:len(o.Progress)-1]
o.Completion = &Completion{
Error: p.Error,
ChatCompletionResponse: p.ChatCompletionResponse,
ActionResult: p.ActionResult,
AgentState: p.AgentState,
}
}

41
core/types/state.go Normal file
View File

@@ -0,0 +1,41 @@
package types
import "fmt"
// State is the structure
// that is used to keep track of the current state
// and the Agent's short memory that it can update
// Besides a long term memory that is accessible by the agent (With vector database),
// And a context memory (that is always powered by a vector database),
// this memory is the shorter one that the LLM keeps across conversation and across its
// reasoning process's and life time.
// TODO: A special action is then used to let the LLM itself update its memory
// periodically during self-processing, and the same action is ALSO exposed
// during the conversation to let the user put for example, a new goal to the agent.
type AgentInternalState struct {
NowDoing string `json:"doing_now"`
DoingNext string `json:"doing_next"`
DoneHistory []string `json:"done_history"`
Memories []string `json:"memories"`
Goal string `json:"goal"`
}
const fmtT = `=====================
NowDoing: %s
DoingNext: %s
Your current goal is: %s
You have done: %+v
You have a short memory with: %+v
=====================
`
func (c AgentInternalState) String() string {
return fmt.Sprintf(
fmtT,
c.NowDoing,
c.DoingNext,
c.Goal,
c.DoneHistory,
c.Memories,
)
}

View File

@@ -7,7 +7,7 @@ services:
# Image list (dockerhub): https://hub.docker.com/r/localai/localai # Image list (dockerhub): https://hub.docker.com/r/localai/localai
image: localai/localai:master-ffmpeg-core image: localai/localai:master-ffmpeg-core
command: command:
- ${MODEL_NAME:-arcee-agent} - ${MODEL_NAME:-gemma-3-12b-it-qat}
- ${MULTIMODAL_MODEL:-minicpm-v-2_6} - ${MULTIMODAL_MODEL:-minicpm-v-2_6}
- ${IMAGE_MODEL:-sd-1.5-ggml} - ${IMAGE_MODEL:-sd-1.5-ggml}
- granite-embedding-107m-multilingual - granite-embedding-107m-multilingual
@@ -59,7 +59,7 @@ services:
- 8080:3000 - 8080:3000
#image: quay.io/mudler/localagi:master #image: quay.io/mudler/localagi:master
environment: environment:
- LOCALAGI_MODEL=${MODEL_NAME:-arcee-agent} - LOCALAGI_MODEL=${MODEL_NAME:-gemma-3-12b-it-qat}
- LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-minicpm-v-2_6} - LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-minicpm-v-2_6}
- LOCALAGI_IMAGE_MODEL=${IMAGE_MODEL:-sd-1.5-ggml} - LOCALAGI_IMAGE_MODEL=${IMAGE_MODEL:-sd-1.5-ggml}
- LOCALAGI_LLM_API_URL=http://localai:8080 - LOCALAGI_LLM_API_URL=http://localai:8080

4
go.mod
View File

@@ -15,11 +15,11 @@ require (
github.com/gofiber/template/html/v2 v2.1.3 github.com/gofiber/template/html/v2 v2.1.3
github.com/google/go-github/v69 v69.2.0 github.com/google/go-github/v69 v69.2.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/metoro-io/mcp-golang v0.9.0 github.com/metoro-io/mcp-golang v0.11.0
github.com/onsi/ginkgo/v2 v2.23.4 github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0 github.com/onsi/gomega v1.37.0
github.com/philippgille/chromem-go v0.7.0 github.com/philippgille/chromem-go v0.7.0
github.com/sashabaranov/go-openai v1.38.1 github.com/sashabaranov/go-openai v1.38.2
github.com/slack-go/slack v0.16.0 github.com/slack-go/slack v0.16.0
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
github.com/tmc/langchaingo v0.1.13 github.com/tmc/langchaingo v0.1.13

8
go.sum
View File

@@ -144,8 +144,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/metoro-io/mcp-golang v0.9.0 h1:GpFENjieZ/KosTu7CE7tyGI/a2FhiG0nandR0d8B3rE= github.com/metoro-io/mcp-golang v0.11.0 h1:1k+VSE9QaeMTLn0gJ3FgE/DcjsCBsLFnz5eSFbgXUiI=
github.com/metoro-io/mcp-golang v0.9.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0= github.com/metoro-io/mcp-golang v0.11.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -179,8 +179,8 @@ github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGK
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY= github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= 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/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sashabaranov/go-openai v1.38.1 h1:TtZabbFQZa1nEni/IhVtDF/WQjVqDgd+cWR5OeddzF8= github.com/sashabaranov/go-openai v1.38.2 h1:akrssjj+6DY3lWuDwHv6cBvJ8Z+FZDM9XEaaYFt0Auo=
github.com/sashabaranov/go-openai v1.38.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/sashabaranov/go-openai v1.38.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/slack-go/slack v0.16.0 h1:khp/WCFv+Hb/B/AJaAwvcxKun0hM6grN0bUZ8xG60P8= github.com/slack-go/slack v0.16.0 h1:khp/WCFv+Hb/B/AJaAwvcxKun0hM6grN0bUZ8xG60P8=
github.com/slack-go/slack v0.16.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/slack-go/slack v0.16.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=

View File

@@ -22,6 +22,7 @@ var withLogs = os.Getenv("LOCALAGI_ENABLE_CONVERSATIONS_LOGGING") == "true"
var apiKeysEnv = os.Getenv("LOCALAGI_API_KEYS") var apiKeysEnv = os.Getenv("LOCALAGI_API_KEYS")
var imageModel = os.Getenv("LOCALAGI_IMAGE_MODEL") var imageModel = os.Getenv("LOCALAGI_IMAGE_MODEL")
var conversationDuration = os.Getenv("LOCALAGI_CONVERSATION_DURATION") var conversationDuration = os.Getenv("LOCALAGI_CONVERSATION_DURATION")
var localOperatorBaseURL = os.Getenv("LOCALOPERATOR_BASE_URL")
func init() { func init() {
if baseModel == "" { if baseModel == "" {
@@ -61,7 +62,9 @@ func main() {
apiKey, apiKey,
stateDir, stateDir,
localRAG, localRAG,
services.Actions, services.Actions(map[string]string{
"browser-agent-runner-base-url": localOperatorBaseURL,
}),
services.Connectors, services.Connectors,
services.DynamicPrompts, services.DynamicPrompts,
timeout, timeout,

View File

@@ -0,0 +1,72 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
// Client represents a client for interacting with the LocalOperator API
type Client struct {
baseURL string
httpClient *http.Client
}
// NewClient creates a new API client
func NewClient(baseURL string) *Client {
return &Client{
baseURL: baseURL,
httpClient: &http.Client{},
}
}
// AgentRequest represents the request body for running an agent
type AgentRequest struct {
Goal string `json:"goal"`
MaxAttempts int `json:"max_attempts,omitempty"`
MaxNoActionAttempts int `json:"max_no_action_attempts,omitempty"`
}
// StateDescription represents a single state in the agent's history
type StateDescription struct {
CurrentURL string `json:"current_url"`
PageTitle string `json:"page_title"`
PageContentDescription string `json:"page_content_description"`
Screenshot string `json:"screenshot"`
ScreenshotMimeType string `json:"screenshot_mime_type"` // MIME type of the screenshot (e.g., "image/png")
}
// StateHistory represents the complete history of states during agent execution
type StateHistory struct {
States []StateDescription `json:"states"`
}
// RunAgent sends a request to run an agent with the given goal
func (c *Client) RunBrowserAgent(req AgentRequest) (*StateHistory, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
resp, err := c.httpClient.Post(
fmt.Sprintf("%s/api/browser/run", c.baseURL),
"application/json",
bytes.NewBuffer(body),
)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var state StateHistory
if err := json.NewDecoder(resp.Body).Decode(&state); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &state, nil
}

View File

@@ -18,6 +18,7 @@ const (
// Actions // Actions
ActionSearch = "search" ActionSearch = "search"
ActionCustom = "custom" ActionCustom = "custom"
ActionBrowserAgentRunner = "browser-agent-runner"
ActionGithubIssueLabeler = "github-issue-labeler" ActionGithubIssueLabeler = "github-issue-labeler"
ActionGithubIssueOpener = "github-issue-opener" ActionGithubIssueOpener = "github-issue-opener"
ActionGithubIssueCloser = "github-issue-closer" ActionGithubIssueCloser = "github-issue-closer"
@@ -52,6 +53,7 @@ var AvailableActions = []string{
ActionGithubIssueSearcher, ActionGithubIssueSearcher,
ActionGithubRepositoryGet, ActionGithubRepositoryGet,
ActionGithubGetAllContent, ActionGithubGetAllContent,
ActionBrowserAgentRunner,
ActionGithubRepositoryCreateOrUpdate, ActionGithubRepositoryCreateOrUpdate,
ActionGithubIssueReader, ActionGithubIssueReader,
ActionGithubIssueCommenter, ActionGithubIssueCommenter,
@@ -71,31 +73,34 @@ var AvailableActions = []string{
ActionShellcommand, ActionShellcommand,
} }
func Actions(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action { func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
return func(ctx context.Context, pool *state.AgentPool) []types.Action { return func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
allActions := []types.Action{} return func(ctx context.Context, pool *state.AgentPool) []types.Action {
allActions := []types.Action{}
agentName := a.Name agentName := a.Name
for _, a := range a.Actions { for _, a := range a.Actions {
var config map[string]string var config map[string]string
if err := json.Unmarshal([]byte(a.Config), &config); err != nil { if err := json.Unmarshal([]byte(a.Config), &config); err != nil {
xlog.Error("Error unmarshalling action config", "error", err) xlog.Error("Error unmarshalling action config", "error", err)
continue continue
}
a, err := Action(a.Name, agentName, config, pool, actionsConfigs)
if err != nil {
continue
}
allActions = append(allActions, a)
} }
a, err := Action(a.Name, agentName, config, pool) return allActions
if err != nil {
continue
}
allActions = append(allActions, a)
} }
return allActions
} }
} }
func Action(name, agentName string, config map[string]string, pool *state.AgentPool) (types.Action, error) { func Action(name, agentName string, config map[string]string, pool *state.AgentPool, actionsConfigs map[string]string) (types.Action, error) {
var a types.Action var a types.Action
var err error var err error
@@ -114,6 +119,8 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
a = actions.NewGithubIssueCloser(config) a = actions.NewGithubIssueCloser(config)
case ActionGithubIssueSearcher: case ActionGithubIssueSearcher:
a = actions.NewGithubIssueSearch(config) a = actions.NewGithubIssueSearch(config)
case ActionBrowserAgentRunner:
a = actions.NewBrowserAgentRunner(config, actionsConfigs["browser-agent-runner-base-url"])
case ActionGithubIssueReader: case ActionGithubIssueReader:
a = actions.NewGithubIssueReader(config) a = actions.NewGithubIssueReader(config)
case ActionGithubPRReader: case ActionGithubPRReader:
@@ -169,6 +176,11 @@ func ActionsConfigMeta() []config.FieldGroup {
Label: "Search", Label: "Search",
Fields: actions.SearchConfigMeta(), Fields: actions.SearchConfigMeta(),
}, },
{
Name: "browser-agent-runner",
Label: "Browser Agent Runner",
Fields: actions.BrowserAgentRunnerConfigMeta(),
},
{ {
Name: "generate_image", Name: "generate_image",
Label: "Generate Image", Label: "Generate Image",

View File

@@ -0,0 +1,117 @@
package actions
import (
"context"
"fmt"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
api "github.com/mudler/LocalAGI/pkg/localoperator"
"github.com/sashabaranov/go-openai/jsonschema"
)
type BrowserAgentRunner struct {
baseURL, customActionName string
client *api.Client
}
func NewBrowserAgentRunner(config map[string]string, defaultURL string) *BrowserAgentRunner {
if config["baseURL"] == "" {
config["baseURL"] = defaultURL
}
client := api.NewClient(config["baseURL"])
return &BrowserAgentRunner{
client: client,
baseURL: config["baseURL"],
customActionName: config["customActionName"],
}
}
func (b *BrowserAgentRunner) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
result := api.AgentRequest{}
err := params.Unmarshal(&result)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
}
req := api.AgentRequest{
Goal: result.Goal,
MaxAttempts: result.MaxAttempts,
MaxNoActionAttempts: result.MaxNoActionAttempts,
}
stateHistory, err := b.client.RunBrowserAgent(req)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to run browser agent: %w", err)
}
// Format the state history into a readable string
var historyStr string
// for i, state := range stateHistory.States {
// historyStr += fmt.Sprintf("State %d:\n", i+1)
// historyStr += fmt.Sprintf(" URL: %s\n", state.CurrentURL)
// historyStr += fmt.Sprintf(" Title: %s\n", state.PageTitle)
// historyStr += fmt.Sprintf(" Description: %s\n\n", state.PageContentDescription)
// }
historyStr += fmt.Sprintf(" URL: %s\n", stateHistory.States[len(stateHistory.States)-1].CurrentURL)
historyStr += fmt.Sprintf(" Title: %s\n", stateHistory.States[len(stateHistory.States)-1].PageTitle)
historyStr += fmt.Sprintf(" Description: %s\n\n", stateHistory.States[len(stateHistory.States)-1].PageContentDescription)
return types.ActionResult{
Result: fmt.Sprintf("Browser agent completed successfully. History:\n%s", historyStr),
Metadata: map[string]interface{}{"browser_agent_history": stateHistory},
}, nil
}
func (b *BrowserAgentRunner) Definition() types.ActionDefinition {
actionName := "run_browser_agent"
if b.customActionName != "" {
actionName = b.customActionName
}
description := "Run a browser agent to achieve a specific goal, for example: 'Go to https://www.google.com and search for 'LocalAI', and tell me what's on the first page'"
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"goal": {
Type: jsonschema.String,
Description: "The goal for the browser agent to achieve",
},
"max_attempts": {
Type: jsonschema.Number,
Description: "Maximum number of attempts the agent can make (optional)",
},
"max_no_action_attempts": {
Type: jsonschema.Number,
Description: "Maximum number of attempts without taking an action (optional)",
},
},
Required: []string{"goal"},
}
}
func (a *BrowserAgentRunner) Plannable() bool {
return true
}
// BrowserAgentRunnerConfigMeta returns the metadata for Browser Agent Runner action configuration fields
func BrowserAgentRunnerConfigMeta() []config.Field {
return []config.Field{
{
Name: "baseURL",
Label: "Base URL",
Type: config.FieldTypeText,
Required: false,
HelpText: "Base URL of the LocalOperator API",
},
{
Name: "customActionName",
Label: "Custom Action Name",
Type: config.FieldTypeText,
HelpText: "Custom name for this action",
},
}
}

View File

@@ -77,8 +77,9 @@ func (i *IRC) Start(a *agent.Agent) {
} }
i.conn.UseTLS = false i.conn.UseTLS = false
i.conn.AddCallback("001", func(e *irc.Event) { i.conn.AddCallback("001", func(e *irc.Event) {
xlog.Info("Connected to IRC server", "server", i.server) xlog.Info("Connected to IRC server", "server", i.server, "arguments", e.Arguments)
i.conn.Join(i.channel) i.conn.Join(i.channel)
i.nickname = e.Arguments[0]
xlog.Info("Joined channel", "channel", i.channel) xlog.Info("Joined channel", "channel", i.channel)
}) })
@@ -207,6 +208,13 @@ func (i *IRC) Start(a *agent.Agent) {
// Start the IRC client in a goroutine // Start the IRC client in a goroutine
go i.conn.Loop() go i.conn.Loop()
go func() {
select {
case <-a.Context().Done():
i.conn.Quit()
return
}
}()
} }
// IRCConfigMeta returns the metadata for IRC connector configuration fields // IRCConfigMeta returns the metadata for IRC connector configuration fields

View File

@@ -176,17 +176,7 @@ func (a *App) UpdateAgentConfig(pool *state.AgentPool) func(c *fiber.Ctx) error
return errorJSONMessage(c, err.Error()) return errorJSONMessage(c, err.Error())
} }
// Remove the agent first if err := pool.RecreateAgent(agentName, &newConfig); err != nil {
if err := pool.Remove(agentName); err != nil {
return errorJSONMessage(c, "Error removing agent: "+err.Error())
}
// Create agent with new config
if err := pool.CreateAgent(agentName, &newConfig); err != nil {
// Try to restore the old configuration if update fails
if restoreErr := pool.CreateAgent(agentName, oldConfig); restoreErr != nil {
return errorJSONMessage(c, fmt.Sprintf("Failed to update agent and restore failed: %v, %v", err, restoreErr))
}
return errorJSONMessage(c, "Error updating agent: "+err.Error()) return errorJSONMessage(c, "Error updating agent: "+err.Error())
} }
@@ -370,7 +360,7 @@ func (a *App) Chat(pool *state.AgentPool) func(c *fiber.Ctx) error {
xlog.Error("Error marshaling status message", "error", err) xlog.Error("Error marshaling status message", "error", err)
} else { } else {
manager.Send( manager.Send(
sse.NewMessage(string(statusData)).WithEvent("json_status")) sse.NewMessage(string(statusData)).WithEvent("json_message_status"))
} }
// Process the message asynchronously // Process the message asynchronously
@@ -417,7 +407,7 @@ func (a *App) Chat(pool *state.AgentPool) func(c *fiber.Ctx) error {
xlog.Error("Error marshaling completed status", "error", err) xlog.Error("Error marshaling completed status", "error", err)
} else { } else {
manager.Send( manager.Send(
sse.NewMessage(string(completedData)).WithEvent("json_status")) sse.NewMessage(string(completedData)).WithEvent("json_message_status"))
} }
}() }()
@@ -444,7 +434,7 @@ func (a *App) ExecuteAction(pool *state.AgentPool) func(c *fiber.Ctx) error {
actionName := c.Params("name") actionName := c.Params("name")
xlog.Debug("Executing action", "action", actionName, "config", payload.Config, "params", payload.Params) xlog.Debug("Executing action", "action", actionName, "config", payload.Config, "params", payload.Params)
a, err := services.Action(actionName, "", payload.Config, pool) a, err := services.Action(actionName, "", payload.Config, pool, map[string]string{})
if err != nil { if err != nil {
xlog.Error("Error creating action", "error", err) xlog.Error("Error creating action", "error", err)
return errorJSONMessage(c, err.Error()) return errorJSONMessage(c, err.Error())

View File

@@ -4,6 +4,7 @@
"": { "": {
"name": "react-ui", "name": "react-ui",
"dependencies": { "dependencies": {
"highlight.js": "^11.11.1",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
}, },
@@ -13,11 +14,11 @@
"@types/react-dom": "^19.1.2", "@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.0", "@vitejs/plugin-react": "^4.4.0",
"eslint": "^9.24.0", "eslint": "^9.24.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^6.0.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0", "globals": "^16.0.0",
"react-router-dom": "^7.5.0", "react-router-dom": "^7.5.1",
"vite": "^6.3.1", "vite": "^6.3.2",
}, },
}, },
}, },
@@ -32,14 +33,26 @@
"@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="], "@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="],
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.0", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.0", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA=="],
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.27.0", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/helper-replace-supers": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/traverse": "^7.27.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg=="],
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="], "@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="], "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="],
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="], "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="],
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.26.5", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/traverse": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg=="],
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
@@ -50,6 +63,8 @@
"@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], "@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="],
"@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.25.9", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="],
@@ -194,8 +209,6 @@
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="], "@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
@@ -256,7 +269,7 @@
"eslint": ["eslint@9.24.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.0", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.24.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ=="], "eslint": ["eslint@9.24.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.0", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.24.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@6.0.0", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "@babel/plugin-transform-private-methods": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-NyC3yIC9fazLitYiN8eHykV5wLp/SMuUZMh+sdPSHIeN4ReXIc7if40jtGjDplAgVL/4OkN1d9gneWe9lFZgag=="],
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.19", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ=="], "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.19", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ=="],
@@ -300,6 +313,12 @@
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@@ -374,9 +393,9 @@
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
"react-router": ["react-router@7.5.0", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g=="], "react-router": ["react-router@7.5.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-/jjU3fcYNd2bwz9Q0xt5TwyiyoO8XjSEFXJY4O/lMAlkGTHWuHRAbR9Etik+lSDqMC7A7mz3UlXzgYT6Vl58sA=="],
"react-router-dom": ["react-router-dom@7.5.0", "", { "dependencies": { "react-router": "7.5.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA=="], "react-router-dom": ["react-router-dom@7.5.1", "", { "dependencies": { "react-router": "7.5.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5DPSPc7ENrt2tlKPq0FtpG80ZbqA9aIKEyqX6hSNJDlol/tr6iqCK4crqdsusmOSSotq6zDsn0y3urX9TuTNmA=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
@@ -408,7 +427,7 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"vite": ["vite@6.3.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.3", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.12" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-kkzzkqtMESYklo96HKKPE5KKLkC1amlsqt+RjFMlX2AvbRB/0wghap19NdBxxwGZ+h/C6DLCrcEphPIItlGrRQ=="], "vite": ["vite@6.3.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.3", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.12" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -418,6 +437,10 @@
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zod": ["zod@3.24.3", "", {}, "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg=="],
"zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="],
"@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],

View File

@@ -11,7 +11,8 @@
}, },
"dependencies": { "dependencies": {
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0" "react-dom": "^19.1.0",
"highlight.js": "^11.11.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.24.0", "@eslint/js": "^9.24.0",
@@ -19,10 +20,10 @@
"@types/react-dom": "^19.1.2", "@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.0", "@vitejs/plugin-react": "^4.4.0",
"eslint": "^9.24.0", "eslint": "^9.24.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^6.0.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0", "globals": "^16.0.0",
"react-router-dom": "^7.5.0", "react-router-dom": "^7.5.1",
"vite": "^6.3.1" "vite": "^6.3.2"
} }
} }

View File

@@ -1,4 +1,17 @@
/* Base styles */ /* Base styles */
pre.hljs {
background-color: var(--medium-bg);
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
font-family: 'JetBrains Mono', monospace;
line-height: 1.5;
}
code.json {
display: block;
}
:root { :root {
--primary: #00ff95; --primary: #00ff95;
--secondary: #ff00b1; --secondary: #ff00b1;
@@ -1994,16 +2007,62 @@ select.form-control {
text-decoration: none; text-decoration: none;
} }
.file-button:hover {
background: rgba(0, 255, 149, 0.8);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.file-button i { .file-button i {
font-size: 16px; font-size: 16px;
} }
.card {
background: var(--medium-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
transition: all 0.3s ease;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
cursor: pointer;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
background: var(--light-bg);
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid var(--primary);
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.expand-button {
background: none;
border: none;
color: var(--primary);
cursor: pointer;
font-size: 1.2em;
padding: 5px;
margin-left: 10px;
transition: all 0.3s ease;
}
.expand-button:hover {
color: var(--success);
transform: scale(1.1);
}
.expand-button:focus {
outline: none;
box-shadow: 0 0 0 2px var(--primary);
}
.selected-file-info { .selected-file-info {
margin-top: 20px; margin-top: 20px;
padding: 20px; padding: 20px;

View File

@@ -63,8 +63,8 @@ export function useSSE(agentName) {
} }
}); });
// Handle 'json_status' event // Handle 'json_message_status' event
eventSource.addEventListener('json_status', (event) => { eventSource.addEventListener('json_message_status', (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
const timestamp = data.timestamp || new Date().toISOString(); const timestamp = data.timestamp || new Date().toISOString();

View File

@@ -1,13 +1,22 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom'; import { useParams, Link } from 'react-router-dom';
import hljs from 'highlight.js/lib/core';
import json from 'highlight.js/lib/languages/json';
import 'highlight.js/styles/monokai.css';
hljs.registerLanguage('json', json);
function AgentStatus() { function AgentStatus() {
const [showStatus, setShowStatus] = useState(true);
const { name } = useParams(); const { name } = useParams();
const [statusData, setStatusData] = useState(null); const [statusData, setStatusData] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [_eventSource, setEventSource] = useState(null); const [_eventSource, setEventSource] = useState(null);
const [liveUpdates, setLiveUpdates] = useState([]); // Store all observables by id
const [observableMap, setObservableMap] = useState({});
const [observableTree, setObservableTree] = useState([]);
const [expandedCards, setExpandedCards] = useState(new Map());
// Update document title // Update document title
useEffect(() => { useEffect(() => {
@@ -39,17 +48,89 @@ function AgentStatus() {
fetchStatusData(); fetchStatusData();
// Helper to build observable tree from map
function buildObservableTree(map) {
const nodes = Object.values(map);
const nodeMap = {};
nodes.forEach(node => { nodeMap[node.id] = { ...node, children: [] }; });
const roots = [];
nodes.forEach(node => {
if (!node.parent_id) {
roots.push(nodeMap[node.id]);
} else if (nodeMap[node.parent_id]) {
nodeMap[node.parent_id].children.push(nodeMap[node.id]);
}
});
return roots;
}
// Fetch initial observable history
const fetchObservables = async () => {
try {
const response = await fetch(`/api/agent/${name}/observables`);
if (!response.ok) return;
const data = await response.json();
if (Array.isArray(data.History)) {
const map = {};
data.History.forEach(obs => {
map[obs.id] = obs;
});
setObservableMap(map);
setObservableTree(buildObservableTree(map));
}
} catch (err) {
// Ignore errors for now
}
};
fetchObservables();
// Setup SSE connection for live updates // Setup SSE connection for live updates
const sse = new EventSource(`/sse/${name}`); const sse = new EventSource(`/sse/${name}`);
setEventSource(sse); setEventSource(sse);
sse.addEventListener('observable_update', (event) => {
const data = JSON.parse(event.data);
console.log(data);
setObservableMap(prevMap => {
const prev = prevMap[data.id] || {};
const updated = {
...prev,
...data,
creation: data.creation,
progress: data.progress,
completion: data.completion,
};
// Events can be received out of order
if (data.creation)
updated.creation = data.creation;
if (data.completion)
updated.completion = data.completion;
if (data.parent_id && !prevMap[data.parent_id])
prevMap[data.parent_id] = {
id: data.parent_id,
name: "unknown",
};
const newMap = { ...prevMap, [data.id]: updated };
setObservableTree(buildObservableTree(newMap));
return newMap;
});
});
// Listen for status events and append to statusData.History
sse.addEventListener('status', (event) => { sse.addEventListener('status', (event) => {
try { const status = event.data;
const data = JSON.parse(event.data); setStatusData(prev => {
setLiveUpdates(prev => [data, ...prev.slice(0, 19)]); // Keep last 20 updates // If prev is null, start a new object
} catch (err) { if (!prev || typeof prev !== 'object') {
setLiveUpdates(prev => [event.data, ...prev.slice(0, 19)]); return { History: [status] };
} }
// If History not present, add it
if (!Array.isArray(prev.History)) {
return { ...prev, History: [status] };
}
// Otherwise, append
return { ...prev, History: [...prev.History, status] };
});
}); });
sse.onerror = (err) => { sse.onerror = (err) => {
@@ -83,8 +164,8 @@ function AgentStatus() {
if (loading) { if (loading) {
return ( return (
<div className="loading-container"> <div>
<div className="loader"></div> <div></div>
<p>Loading agent status...</p> <p>Loading agent status...</p>
</div> </div>
); );
@@ -92,56 +173,199 @@ function AgentStatus() {
if (error) { if (error) {
return ( return (
<div className="error-container"> <div>
<h2>Error</h2> <h2>Error</h2>
<p>{error}</p> <p>{error}</p>
<Link to="/agents" className="back-btn"> <Link to="/agents">
<i className="fas fa-arrow-left"></i> Back to Agents <i className="fas fa-arrow-left"></i> Back to Agents
</Link> </Link>
</div> </div>
); );
} }
// Combine live updates with history
const allUpdates = [...liveUpdates, ...(statusData?.History || [])];
return ( return (
<div className="agent-status-container"> <div>
<header className="page-header"> <h1>Agent Status: {name}</h1>
<div className="header-content"> <div style={{ color: '#aaa', fontSize: 16, marginBottom: 18 }}>
<h1> See what the agent is doing and thinking
<Link to="/agents" className="back-link"> </div>
<i className="fas fa-arrow-left"></i> {error && (
</Link> <div>
Agent Status: {name} {error}
</h1>
</div> </div>
</header> )}
{loading && <div>Loading...</div>}
{statusData && (
<div>
<div>
<div style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', userSelect: 'none' }}
onClick={() => setShowStatus(prev => !prev)}>
<h2 style={{ margin: 0 }}>Current Status</h2>
<i
className={`fas fa-chevron-${showStatus ? 'up' : 'down'}`}
style={{ color: 'var(--primary)', marginLeft: 12 }}
title={showStatus ? 'Collapse' : 'Expand'}
/>
</div>
<div style={{ color: '#aaa', fontSize: 14, margin: '5px 0 10px 2px' }}>
Summary of the agent's thoughts and actions
</div>
{showStatus && (
<div style={{ marginTop: 10 }}>
{(Array.isArray(statusData?.History) && statusData.History.length === 0) && (
<div style={{ color: '#aaa' }}>No status history available.</div>
)}
{Array.isArray(statusData?.History) && statusData.History.map((item, idx) => (
<div key={idx} style={{
background: '#222',
border: '1px solid #444',
borderRadius: 8,
padding: '12px 16px',
marginBottom: 10,
whiteSpace: 'pre-line',
fontFamily: 'inherit',
fontSize: 15,
color: '#eee',
}}>
{/* Replace <br> tags with newlines, then render as pre-line */}
{typeof item === 'string'
? item.replace(/<br\s*\/?>/gi, '\n')
: JSON.stringify(item)}
</div>
))}
</div>
)}
</div>
{observableTree.length > 0 && (
<div>
<h2>Observable Updates</h2>
<div style={{ color: '#aaa', fontSize: 14, margin: '5px 0 10px 2px' }}>
Drill down into what the agent is doing and thinking when activated by a connector
</div>
<div>
{observableTree.map((container, idx) => (
<div key={container.id || idx} className='card' style={{ marginBottom: '1em' }}>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => {
const newExpanded = !expandedCards.get(container.id);
setExpandedCards(new Map(expandedCards).set(container.id, newExpanded));
}}
>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<i className={`fas fa-${container.icon || 'robot'}`} style={{ verticalAlign: '-0.125em' }}></i>
<span>
<span className='stat-label'>{container.name}</span>#<span className='stat-label'>{container.id}</span>
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<i
className={`fas fa-chevron-${expandedCards.get(container.id) ? 'up' : 'down'}`}
style={{ color: 'var(--primary)' }}
title='Toggle details'
/>
{!container.completion && (
<div className='spinner' />
)}
</div>
</div>
<div style={{ display: expandedCards.get(container.id) ? 'block' : 'none' }}>
{container.children && container.children.length > 0 && (
<div className="chat-container bg-gray-800 shadow-lg rounded-lg"> <div style={{ marginLeft: '2em', marginTop: '1em' }}>
{/* Chat Messages */} <h4>Nested Observables</h4>
<div className="chat-messages p-4"> {container.children.map(child => {
{allUpdates.length > 0 ? ( const childKey = `child-${child.id}`;
allUpdates.map((item, index) => ( const isExpanded = expandedCards.get(childKey);
<div key={index} className="status-item mb-4"> return (
<div className="bg-gray-700 p-4 rounded-lg"> <div key={`${container.id}-child-${child.id}`} className='card' style={{ background: '#222', marginBottom: '0.5em' }}>
<h2 className="text-sm font-semibold mb-2">Agent Action:</h2> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
<div className="status-details"> onClick={() => {
<div className="status-row"> const newExpanded = !expandedCards.get(childKey);
<span className="status-label">{index}</span> setExpandedCards(new Map(expandedCards).set(childKey, newExpanded));
<span className="status-value">{formatValue(item)}</span> }}
>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<i className={`fas fa-${child.icon || 'robot'}`} style={{ verticalAlign: '-0.125em' }}></i>
<span>
<span className='stat-label'>{child.name}</span>#<span className='stat-label'>{child.id}</span>
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<i
className={`fas fa-chevron-${isExpanded ? 'up' : 'down'}`}
style={{ color: 'var(--primary)' }}
title='Toggle details'
/>
{!child.completion && (
<div className='spinner' />
)}
</div>
</div>
<div style={{ display: isExpanded ? 'block' : 'none' }}>
{child.creation && (
<div>
<h5>Creation:</h5>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(child.creation || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
{child.progress && child.progress.length > 0 && (
<div>
<h5>Progress:</h5>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(child.progress || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
{child.completion && (
<div>
<h5>Completion:</h5>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(child.completion || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
</div>
</div>
);
})}
</div>
)}
{container.creation && (
<div>
<h4>Creation:</h4>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.creation || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
{container.progress && container.progress.length > 0 && (
<div>
<h4>Progress:</h4>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.progress || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
{container.completion && (
<div>
<h4>Completion:</h4>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.completion || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
</div>
</div> </div>
</div> </div>
</div> ))}
</div> </div>
))
) : (
<div className="no-status-data">
<p>No status data available for this agent.</p>
</div> </div>
)} )}
</div> </div>
</div> )}
</div> </div>
); );
} }

View File

@@ -241,13 +241,14 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
entries := []string{} entries := []string{}
for _, h := range Reverse(history.Results()) { for _, h := range Reverse(history.Results()) {
entries = append(entries, fmt.Sprintf( entries = append(entries, fmt.Sprintf(`Reasoning: %s
"Result: %v Action: %v Params: %v Reasoning: %v", Action taken: %+v
h.Result, Parameters: %+v
h.Action.Definition().Name, Result: %s`,
h.Params,
h.Reasoning, h.Reasoning,
)) h.ActionCurrentState.Action.Definition().Name,
h.ActionCurrentState.Params,
h.Result))
} }
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
@@ -256,6 +257,21 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
}) })
}) })
webapp.Get("/api/agent/:name/observables", func(c *fiber.Ctx) error {
name := c.Params("name")
agent := pool.GetAgent(name)
if agent == nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": "Agent not found",
})
}
return c.JSON(fiber.Map{
"Name": name,
"History": agent.Observer().History(),
})
})
webapp.Post("/settings/import", app.ImportAgent(pool)) webapp.Post("/settings/import", app.ImportAgent(pool))
webapp.Get("/settings/export/:name", app.ExportAgent(pool)) webapp.Get("/settings/export/:name", app.ExportAgent(pool))