Compare commits

...

68 Commits

Author SHA1 Message Date
dependabot[bot] 3a5ea024eb chore(deps): bump github.com/mudler/cogito from 0.4.2 to 0.5.1
Bumps [github.com/mudler/cogito](https://github.com/mudler/cogito) from 0.4.2 to 0.5.1.
- [Release notes](https://github.com/mudler/cogito/releases)
- [Commits](https://github.com/mudler/cogito/compare/v0.4.2...v0.5.1)

---
updated-dependencies:
- dependency-name: github.com/mudler/cogito
  dependency-version: 0.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 18:28:59 +00:00
dependabot[bot] fbb098e996 chore(deps-dev): bump @eslint/js from 9.38.0 to 9.39.0 in /webui/react-ui (#342)
chore(deps-dev): bump @eslint/js in /webui/react-ui

Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.38.0 to 9.39.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v9.39.0/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.39.0
  dependency-type: direct:development
  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-11-10 09:31:29 +01:00
dependabot[bot] b124244b0c chore(deps-dev): bump react-router-dom from 7.9.4 to 7.9.5 in /webui/react-ui (#341)
chore(deps-dev): bump react-router-dom in /webui/react-ui

Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.9.4 to 7.9.5.
- [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.9.5/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.9.5
  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-11-10 09:31:19 +01:00
dependabot[bot] c7d1b58340 chore(deps-dev): bump eslint from 9.38.0 to 9.39.0 in /webui/react-ui (#343)
Bumps [eslint](https://github.com/eslint/eslint) from 9.38.0 to 9.39.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.38.0...v9.39.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.39.0
  dependency-type: direct:development
  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-11-03 22:11:37 +01:00
dependabot[bot] 9971819350 chore(deps-dev): bump globals from 16.3.0 to 16.5.0 in /webui/react-ui (#344)
Bumps [globals](https://github.com/sindresorhus/globals) from 16.3.0 to 16.5.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.3.0...v16.5.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 16.5.0
  dependency-type: direct:development
  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-11-03 22:11:22 +01:00
dependabot[bot] e3769d3e1f chore(deps): bump github.com/modelcontextprotocol/go-sdk from 1.0.0 to 1.1.0 (#346)
chore(deps): bump github.com/modelcontextprotocol/go-sdk

Bumps [github.com/modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk) from 1.0.0 to 1.1.0.
- [Release notes](https://github.com/modelcontextprotocol/go-sdk/releases)
- [Commits](https://github.com/modelcontextprotocol/go-sdk/compare/v1.0.0...v1.1.0)

---
updated-dependencies:
- dependency-name: github.com/modelcontextprotocol/go-sdk
  dependency-version: 1.1.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-11-03 22:10:56 +01:00
dependabot[bot] f777ce2992 chore(deps): bump python from 3.13-slim to 3.14-slim (#338)
Bumps python from 3.13-slim to 3.14-slim.

---
updated-dependencies:
- dependency-name: python
  dependency-version: 3.14-slim
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-01 20:57:44 +01:00
Ettore Di Giacinto 02eda4efb3 feat(prompts): support Sprig in templates (#337)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-10-31 22:44:56 +01:00
Ettore Di Giacinto 1b3420c857 chore(deps): bump cogito
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-10-28 18:48:20 +01:00
dependabot[bot] 2f537e72bb chore(deps-dev): bump eslint-plugin-react-hooks from 6.0.0 to 7.0.1 in /webui/react-ui (#330)
chore(deps-dev): bump eslint-plugin-react-hooks in /webui/react-ui

Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 6.0.0 to 7.0.1.
- [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: 7.0.1
  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-10-28 11:03:56 +01:00
dependabot[bot] 4afdb6f813 chore(deps): bump github.com/tmc/langchaingo from 0.1.13 to 0.1.14 (#335)
Bumps [github.com/tmc/langchaingo](https://github.com/tmc/langchaingo) from 0.1.13 to 0.1.14.
- [Release notes](https://github.com/tmc/langchaingo/releases)
- [Commits](https://github.com/tmc/langchaingo/compare/v0.1.13...v0.1.14)

---
updated-dependencies:
- dependency-name: github.com/tmc/langchaingo
  dependency-version: 0.1.14
  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-10-28 11:03:46 +01:00
dependabot[bot] c610c2c6ca chore(deps): bump github.com/gofiber/fiber/v2 from 2.52.8 to 2.52.9 (#306)
Bumps [github.com/gofiber/fiber/v2](https://github.com/gofiber/fiber) from 2.52.8 to 2.52.9.
- [Release notes](https://github.com/gofiber/fiber/releases)
- [Commits](https://github.com/gofiber/fiber/compare/v2.52.8...v2.52.9)

---
updated-dependencies:
- dependency-name: github.com/gofiber/fiber/v2
  dependency-version: 2.52.9
  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-10-28 09:17:07 +01:00
dependabot[bot] 047a324e0e chore(deps-dev): bump @eslint/js from 9.37.0 to 9.38.0 in /webui/react-ui (#333)
chore(deps-dev): bump @eslint/js in /webui/react-ui

Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.37.0 to 9.38.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v9.38.0/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.38.0
  dependency-type: direct:development
  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-10-28 09:16:45 +01:00
dependabot[bot] e37283cb11 chore(deps): bump github.com/valyala/fasthttp from 1.62.0 to 1.68.0 (#334)
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.62.0 to 1.68.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.62.0...v1.68.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-version: 1.68.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-10-27 21:34:18 +01:00
dependabot[bot] d6f10b28c1 chore(deps-dev): bump vite from 7.1.4 to 7.1.12 in /webui/react-ui (#331)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.4 to 7.1.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.1.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.12
  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-10-27 21:33:35 +01:00
dependabot[bot] c082b42395 chore(deps-dev): bump @vitejs/plugin-react from 5.0.4 to 5.1.0 in /webui/react-ui (#332)
chore(deps-dev): bump @vitejs/plugin-react in /webui/react-ui

Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 5.0.4 to 5.1.0.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.1.0/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.1.0
  dependency-type: direct:development
  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-10-27 21:33:23 +01:00
dependabot[bot] 252ee2aff6 chore(deps-dev): bump react-router-dom from 7.7.0 to 7.9.4 in /webui/react-ui (#323)
chore(deps-dev): bump react-router-dom in /webui/react-ui

Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.7.0 to 7.9.4.
- [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.9.4/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.9.4
  dependency-type: direct:development
  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-10-27 19:05:44 +01:00
dependabot[bot] fffc1f8647 chore(deps): bump react and @types/react in /webui/react-ui (#322)
Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react). These dependencies needed to be updated together.

Updates `react` from 19.1.0 to 19.2.0
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.0/packages/react)

Updates `@types/react` from 19.1.8 to 19.2.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: "@types/react"
  dependency-version: 19.2.2
  dependency-type: direct:development
  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-10-27 19:05:35 +01:00
dependabot[bot] dc1653294d chore(deps): bump actions/setup-go from 5 to 6 (#308)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  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-10-27 19:01:46 +01:00
dependabot[bot] 3862c3c0eb chore(deps-dev): bump eslint from 9.35.0 to 9.38.0 in /webui/react-ui (#326)
Bumps [eslint](https://github.com/eslint/eslint) from 9.35.0 to 9.38.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.35.0...v9.38.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.38.0
  dependency-type: direct:development
  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-10-27 19:01:32 +01:00
dependabot[bot] 756c46b526 chore(deps-dev): bump eslint-plugin-react-refresh from 0.4.20 to 0.4.24 in /webui/react-ui (#327)
chore(deps-dev): bump eslint-plugin-react-refresh in /webui/react-ui

Bumps [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) from 0.4.20 to 0.4.24.
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.4.20...v0.4.24)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.4.24
  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-10-27 19:01:14 +01:00
Ettore Di Giacinto 73a6be8264 feat: consume cogito for agent reasoning (#320)
* WIP

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

* Drop old webui

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

* Almost there

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

* It builds

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

* Make it build

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

* fixups, still doesn't work

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

* unused now

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

* Send result before closing

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

* Fix observability

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

* Drop MCP code and wire-up in cogito

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

* Drop some templates

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

* Keep reporting into conv

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

* tests fixups

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

* tests fixups

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

* fixups

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

* Do not complete observable during thought process

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

* Update cogito

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

* Fixups

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

* Fixups

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

* Fixups

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

* Drop unneded option now

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

* Fixups

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

* Better handling of user tools

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

* TEST

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

* Add flake attempts

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

* Revert "TEST"

This reverts commit 8b12a9fd03.

* tKeep indexing MCP actions

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

* Split CI jobs to improve speed

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

* CI optimizations

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

* Bump timeout

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

* Bump cogito

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

* fix: always commit last progress

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

* chore: better management of observables

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-10-27 18:41:28 +01:00
dependabot[bot] 84f21c6ab4 chore(deps): bump react-dom and @types/react-dom in /webui/react-ui (#324)
Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) and [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom). These dependencies needed to be updated together.

Updates `react-dom` from 19.1.0 to 19.2.0
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.0/packages/react-dom)

Updates `@types/react-dom` from 19.1.6 to 19.2.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.2
  dependency-type: direct:development
  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-10-16 18:52:24 +02:00
dependabot[bot] a0ab067bf7 chore(deps-dev): bump @vitejs/plugin-react from 5.0.2 to 5.0.4 in /webui/react-ui (#325)
chore(deps-dev): bump @vitejs/plugin-react in /webui/react-ui

Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 5.0.2 to 5.0.4.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.0.4/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.0.4
  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-10-14 19:06:25 +02:00
Ettore Di Giacinto c719cbd544 Remove mcpbox service from docker-compose.nvidia.yaml (#319)
Removed mcpbox service extension from NVIDIA compose file.


Leftover of #318
2025-10-09 09:23:15 +02:00
Ettore Di Giacinto 6fa73262fc feat: switch to official MCP SDK (#318)
* feat: switch to official MCP SDK

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

* Drop MCP Box

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-10-08 17:14:03 +02:00
Ettore Di Giacinto 558fa396a8 feat: add audio support (#316)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-10-07 12:00:33 +02:00
dependabot[bot] 33ffe06510 chore(deps-dev): bump @eslint/js from 9.35.0 to 9.37.0 in /webui/react-ui (#314)
chore(deps-dev): bump @eslint/js in /webui/react-ui

Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.35.0 to 9.37.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v9.37.0/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.37.0
  dependency-type: direct:development
  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-10-07 09:23:46 +02:00
Michel Weimerskirch a5a602c5b0 Add "Play" button to action configuration for testing in Playground (#315) 2025-10-07 09:23:26 +02:00
Michel Weimerskirch 500862b768 Add "clear history" option to the agent observables (#312) 2025-10-03 23:06:21 +02:00
Michel Weimerskirch b06522d1ec Add custom name, description, and payload description to WebhookAction (#311) 2025-10-03 23:05:14 +02:00
Michel Weimerskirch 0c312c9caa Update README with detailed instructions for development (#310) 2025-10-03 23:04:23 +02:00
Michel Weimerskirch d456315f19 Add webhook action for HTTP request integration (#309)
* Add Webhook action handling

Integrates a new "webhook" action, enabling configurable HTTP requests with customizable methods, content types, and payloads through runtime parameters.

* Enhance Webhook action documentation and runtime handling

Improves comments and function-level documentation for the Webhook action, clarifying its configuration, runtime parameter handling, and operational behavior. Adds detailed explanations for methods, payload templates, and HTTP request construction.
2025-10-03 09:14:08 +02:00
dependabot[bot] 983f5599d5 chore(deps): bump github.com/onsi/ginkgo/v2 from 2.23.4 to 2.25.3 (#303)
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.23.4 to 2.25.3.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.23.4...v2.25.3)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-version: 2.25.3
  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-09-24 10:52:02 +02:00
dependabot[bot] 471c937d57 chore(deps): bump github.com/JohannesKaufmann/html-to-markdown/v2 from 2.3.3 to 2.4.0 (#286)
chore(deps): bump github.com/JohannesKaufmann/html-to-markdown/v2

Bumps [github.com/JohannesKaufmann/html-to-markdown/v2](https://github.com/JohannesKaufmann/html-to-markdown) from 2.3.3 to 2.4.0.
- [Release notes](https://github.com/JohannesKaufmann/html-to-markdown/releases)
- [Changelog](https://github.com/JohannesKaufmann/html-to-markdown/blob/main/.goreleaser.yaml)
- [Commits](https://github.com/JohannesKaufmann/html-to-markdown/compare/v2.3.3...v2.4.0)

---
updated-dependencies:
- dependency-name: github.com/JohannesKaufmann/html-to-markdown/v2
  dependency-version: 2.4.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-09-19 18:14:52 +02:00
dependabot[bot] ad61551db5 chore(deps-dev): bump @vitejs/plugin-react from 4.7.0 to 5.0.2 in /webui/react-ui (#288)
chore(deps-dev): bump @vitejs/plugin-react in /webui/react-ui

Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.7.0 to 5.0.2.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.0.2/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.0.2
  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-09-19 18:14:36 +02:00
dependabot[bot] 2bad9c399f chore(deps-dev): bump @eslint/js from 9.33.0 to 9.35.0 in /webui/react-ui (#298)
chore(deps-dev): bump @eslint/js in /webui/react-ui

Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.33.0 to 9.35.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/commits/v9.35.0/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.35.0
  dependency-type: direct:development
  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-09-16 15:43:21 +02:00
dependabot[bot] 580ad7d46f chore(deps): bump github.com/sashabaranov/go-openai from 1.40.3 to 1.41.2 (#296)
chore(deps): bump github.com/sashabaranov/go-openai

Bumps [github.com/sashabaranov/go-openai](https://github.com/sashabaranov/go-openai) from 1.40.3 to 1.41.2.
- [Release notes](https://github.com/sashabaranov/go-openai/releases)
- [Commits](https://github.com/sashabaranov/go-openai/compare/v1.40.3...v1.41.2)

---
updated-dependencies:
- dependency-name: github.com/sashabaranov/go-openai
  dependency-version: 1.41.2
  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-09-16 15:42:42 +02:00
dependabot[bot] 47f013f3e1 chore(deps-dev): bump eslint from 9.34.0 to 9.35.0 in /webui/react-ui (#299)
Bumps [eslint](https://github.com/eslint/eslint) from 9.34.0 to 9.35.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.34.0...v9.35.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.35.0
  dependency-type: direct:development
  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-09-16 15:42:18 +02:00
mudler d5219a9379 Update example 2025-09-13 16:42:25 +02:00
mudler 00460b1538 fix: do not change name field
Signed-off-by: mudler <mudler@localai.io>
2025-09-13 16:33:08 +02:00
Ettore Di Giacinto 98e62edf12 feat(actions): add action to control pikvm (#294)
Signed-off-by: mudler <mudler@localai.io>
2025-09-13 16:28:42 +02:00
Ettore Di Giacinto 592969f976 feat(actions/dynamicprompt): allow to pass-by a configuration (#293)
This is useful mainly to pass-by from the web interface a generic string
that can be later re-used in the custom actions or dynamic prompt during
Init().

The Signature of the custom actions and prompts around Init has changed,
and now is Init(string), this is on the custom action duty to decide if
the string is e.g. a JSON, or a comma separated list, etc.

Signed-off-by: mudler <mudler@localai.io>
2025-09-13 15:50:25 +02:00
Ettore Di Giacinto 6be849e409 Fix image tag for localai with CUDA 12 2025-09-12 12:26:01 +02:00
Ettore Di Giacinto d42c2a5749 Update Docker image tag for localai service 2025-09-12 12:25:44 +02:00
Ettore Di Giacinto 8d2eafbc35 chore: propagate env (#292)
Signed-off-by: mudler <mudler@localai.io>
2025-09-11 18:16:51 +02:00
Ettore Di Giacinto 848f688ae9 Scan for dynamic prompts in custom action folder (#291)
Signed-off-by: mudler <mudler@localai.io>
2025-09-10 23:45:46 +02:00
dependabot[bot] b813842f85 chore(deps-dev): bump eslint from 9.31.0 to 9.34.0 in /webui/react-ui (#282)
Bumps [eslint](https://github.com/eslint/eslint) from 9.31.0 to 9.34.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.31.0...v9.34.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.34.0
  dependency-type: direct:development
  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-09-09 23:26:36 +02:00
Ettore Di Giacinto 684ce43eb8 feat: preload custom actions from dir (#290)
* feat: preload custom actions from dir

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

* fix(custom dir): allow to override name and description, strip package from code

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Signed-off-by: mudler <mudler@localai.io>
2025-09-09 18:05:15 +02:00
dependabot[bot] 39d13d155f chore(deps-dev): bump vite from 7.0.6 to 7.1.4 in /webui/react-ui (#287)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.0.6 to 7.1.4.
- [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/v7.1.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.4
  dependency-type: direct:development
  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-09-02 21:02:16 +02:00
dependabot[bot] 2d5523a129 chore(deps): bump github.com/metoro-io/mcp-golang from 0.14.0 to 0.16.0 (#279)
Bumps [github.com/metoro-io/mcp-golang](https://github.com/metoro-io/mcp-golang) from 0.14.0 to 0.16.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.14.0...v0.16.0)

---
updated-dependencies:
- dependency-name: github.com/metoro-io/mcp-golang
  dependency-version: 0.16.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-08-29 21:54:03 +02:00
Ettore Di Giacinto 7fe3ed2169 feat(custom actions): allow to specify a dir with all custom actions (#277)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-08-25 09:33:21 +02:00
dependabot[bot] 6db3b5f31f chore(deps-dev): bump @eslint/js from 9.31.0 to 9.33.0 in /webui/react-ui (#263)
chore(deps-dev): bump @eslint/js in /webui/react-ui

Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.31.0 to 9.33.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/commits/v9.33.0/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.33.0
  dependency-type: direct:development
  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-08-19 08:41:50 +02:00
dependabot[bot] f6ac820a3f chore(deps): bump github.com/emersion/go-smtp from 0.22.0 to 0.24.0 (#273)
Bumps [github.com/emersion/go-smtp](https://github.com/emersion/go-smtp) from 0.22.0 to 0.24.0.
- [Release notes](https://github.com/emersion/go-smtp/releases)
- [Commits](https://github.com/emersion/go-smtp/compare/v0.22.0...v0.24.0)

---
updated-dependencies:
- dependency-name: github.com/emersion/go-smtp
  dependency-version: 0.24.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-08-19 08:41:27 +02:00
dependabot[bot] d955284278 chore(deps): bump github.com/go-telegram/bot from 1.15.0 to 1.17.0 (#270)
Bumps [github.com/go-telegram/bot](https://github.com/go-telegram/bot) from 1.15.0 to 1.17.0.
- [Release notes](https://github.com/go-telegram/bot/releases)
- [Changelog](https://github.com/go-telegram/bot/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-telegram/bot/compare/v1.15.0...v1.17.0)

---
updated-dependencies:
- dependency-name: github.com/go-telegram/bot
  dependency-version: 1.17.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-08-19 08:41:05 +02:00
dependabot[bot] 3fd6134bcd chore(deps): bump github.com/metoro-io/mcp-golang from 0.13.0 to 0.14.0 (#271)
Bumps [github.com/metoro-io/mcp-golang](https://github.com/metoro-io/mcp-golang) from 0.13.0 to 0.14.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.13.0...v0.14.0)

---
updated-dependencies:
- dependency-name: github.com/metoro-io/mcp-golang
  dependency-version: 0.14.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-08-19 08:40:44 +02:00
Ettore Di Giacinto 4d875e9e2a fix: gen memory actions only in DynamicPromptMemory (#259)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-08-07 18:55:29 +02:00
Richard Palethorpe 79b72ec311 fix(ui): Correct typos in dynamic prompts (#258)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-08-05 18:44:08 +02:00
Ettore Di Giacinto 8487459d90 feat: multimodal action and dynamic prompts results (#257)
* feat(actions): allow actions to return images

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

* feat(prompts): allow to have images in dynamic prompts

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

* chore: change signature of dynamic prompts to allow to return base64 images

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

* chore: fixups

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-08-05 18:43:50 +02:00
Ettore Di Giacinto be336adf02 feat(dynamic-prompts): add memory tool to dynamic prompt (#256)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-08-04 16:58:11 +02:00
Ettore Di Giacinto be1acb06fd Update README.md 2025-07-25 23:04:09 +02:00
Richard Palethorpe 26ec479596 fix(ui): Avoid passing object (multimedia message) into React template
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-07-25 17:00:46 +01:00
dependabot[bot] efee3c462d chore(deps-dev): bump vite from 7.0.3 to 7.0.5 in /webui/react-ui (#246)
---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.0.5
  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-07-25 11:12:20 +02:00
Ettore Di Giacinto 4cf52ec698 feat(memories): add action to handle simple memory as flat files (#252)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-07-24 23:17:05 +02:00
dependabot[bot] ba8ea0117e chore(deps-dev): bump @eslint/js from 9.30.1 to 9.31.0 in /webui/react-ui (#240)
chore(deps-dev): bump @eslint/js in /webui/react-ui

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.31.0
  dependency-type: direct:development
  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-07-24 15:19:27 +02:00
dependabot[bot] 36770b70ae chore(deps-dev): bump react-router-dom from 7.6.3 to 7.7.0 in /webui/react-ui (#248)
chore(deps-dev): bump react-router-dom in /webui/react-ui

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.7.0
  dependency-type: direct:development
  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>
Co-authored-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2025-07-24 15:19:09 +02:00
dependabot[bot] 5784be2853 chore(deps-dev): bump eslint from 9.30.1 to 9.31.0 in /webui/react-ui (#241)
---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.31.0
  dependency-type: direct:development
  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-07-24 08:30:58 +02:00
dependabot[bot] 5b93e5c21c chore(deps-dev): bump @vitejs/plugin-react from 4.6.0 to 4.7.0 in /webui/react-ui (#249)
chore(deps-dev): bump @vitejs/plugin-react in /webui/react-ui

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 4.7.0
  dependency-type: direct:development
  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-07-23 22:02:27 +02:00
88 changed files with 3164 additions and 8880 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: 1.24
- name: Run GoReleaser
+1 -73
View File
@@ -83,79 +83,7 @@ jobs:
#tags: ${{ steps.prep.outputs.tags }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
mcpbox-build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Prepare
id: prep
run: |
DOCKER_IMAGE=quay.io/mudler/localagi-mcpbox
# Use branch name as default
VERSION=${GITHUB_REF#refs/heads/}
BINARY_VERSION=$(git describe --always --tags --dirty)
SHORTREF=${GITHUB_SHA::8}
# If this is git tag, use the tag name as a docker tag
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
fi
TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:${SHORTREF}"
# If the VERSION looks like a version number, assume that
# this is the most recent version of the image and also
# tag it 'latest'.
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
TAGS="$TAGS,${DOCKER_IMAGE}:latest"
fi
# Set output parameters.
echo ::set-output name=binary_version::${BINARY_VERSION}
echo ::set-output name=tags::${TAGS}
echo ::set-output name=docker_image::${DOCKER_IMAGE}
- name: Set up QEMU
uses: docker/setup-qemu-action@master
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@master
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: quay.io
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804
with:
images: quay.io/mudler/localagi-mcpbox
tags: |
type=ref,event=branch,suffix=-{{date 'YYYYMMDDHHmmss'}}
type=semver,pattern={{raw}}
type=sha,suffix=-{{date 'YYYYMMDDHHmmss'}}
type=ref,event=branch
flavor: |
latest=auto
prefix=
suffix=
- name: Build
uses: docker/build-push-action@v6
with:
builder: ${{ steps.buildx.outputs.name }}
build-args: |
VERSION=${{ steps.prep.outputs.binary_version }}
context: ./
file: ./Dockerfile.mcpbox
platforms: linux/amd64,linux/arm64
push: true
#tags: ${{ steps.prep.outputs.tags }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
sshbox-build:
runs-on: ubuntu-latest
steps:
+12 -36
View File
@@ -1,4 +1,4 @@
name: Run Go Tests
name: Run Tests
on:
push:
@@ -16,43 +16,19 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- run: |
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin make
docker version
docker run --rm hello-world
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version: '>=1.17.0'
- name: Free up disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo apt-get clean
docker system prune -af || true
df -h
- name: Run tests
run: |
make tests
#sudo mv coverage/coverage.txt coverage.txt
#sudo chmod 777 coverage.txt
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v4
# with:
# token: ${{ secrets.CODECOV_TOKEN }}
test-e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/setup-go@v6
with:
go-version: '>=1.17.0'
- run: |
make tests-e2e
-49
View File
@@ -1,49 +0,0 @@
# Build stage
FROM golang:1.24-alpine AS builder
# Install build dependencies
RUN apk add --no-cache git
# Set working directory
WORKDIR /app
# Copy go mod files
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -o mcpbox ./cmd/mcpbox
# Final stage
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
# Install runtime dependencies
RUN apt-get update && apt-get install -y ca-certificates tzdata docker.io bash wget curl
# Create non-root user
#RUN adduser -D -g '' appuser
# Set working directory
WORKDIR /app
# Copy binary from builder
COPY --from=builder /app/mcpbox .
# Use non-root user
#USER appuser
# Expose port
EXPOSE 8080
# Set entrypoint
ENTRYPOINT ["/app/mcpbox"]
# Default command
CMD ["-addr", ":8080"]
+1 -1
View File
@@ -1,5 +1,5 @@
# python
FROM python:3.13-slim
FROM python:3.14-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y python3-dev portaudio19-dev ffmpeg build-essential
+12 -3
View File
@@ -44,12 +44,21 @@ COPY --from=ui-builder /app/dist /work/webui/react-ui/dist
# Build the application
RUN CGO_ENABLED=0 go build -ldflags="$LDFLAGS" -o localagi ./
FROM scratch
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
ca-certificates \
tzdata \
docker.io \
bash \
wget \
curl
# Copy the webui binary from the builder stage to the final image
COPY --from=builder /work/localagi /localagi
COPY --from=builder /etc/ssl/ /etc/ssl/
COPY --from=builder /tmp /tmp
# Define the command that will be run when the container is started
ENTRYPOINT ["/localagi"]
+5 -10
View File
@@ -1,17 +1,15 @@
GOCMD?=go
IMAGE_NAME?=webui
MCPBOX_IMAGE_NAME?=mcpbox
ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
prepare-tests: build-mcpbox
prepare-tests:
docker compose up -d --build
docker run -d -v /var/run/docker.sock:/var/run/docker.sock --privileged -p 9090:8080 --rm -ti $(MCPBOX_IMAGE_NAME)
cleanup-tests:
docker compose down
tests: prepare-tests
LOCALAGI_MCPBOX_URL="http://localhost:9090" LOCALAGI_MODEL="gemma-3-4b-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 ./...
LOCALAGI_MODEL="gemma-3-4b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter="!E2E" --flake-attempts=5 --fail-fast -v -r ./...
run-nokb:
$(MAKE) run KBDISABLEINDEX=true
@@ -25,7 +23,7 @@ build: webui/react-ui/dist
.PHONY: run
run: webui/react-ui/dist
LOCALAGI_MCPBOX_URL="http://localhost:9090" $(GOCMD) run ./
$(GOCMD) run ./
build-image:
docker build -t $(IMAGE_NAME) -f Dockerfile.webui .
@@ -33,8 +31,5 @@ build-image:
image-push:
docker push $(IMAGE_NAME)
build-mcpbox:
docker build -t $(MCPBOX_IMAGE_NAME) -f Dockerfile.mcpbox .
run-mcpbox:
docker run -v /var/run/docker.sock:/var/run/docker.sock --privileged -p 9090:8080 -ti mcpbox
tests-e2e: prepare-tests
LOCALAGI_MODEL="gemma-3-4b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter="E2E" --flake-attempts=5 --fail-fast -v -r ./tests/e2e/...
+57 -6
View File
@@ -31,7 +31,7 @@ LocalAGI ensures your data stays exactly where you want it—on your hardware. N
- 🎛 **No-Code Agents**: Easy-to-configure multiple agents via Web UI.
- 🖥 **Web-Based Interface**: Simple and intuitive agent management.
- 🤖 **Advanced Agent Teaming**: Instantly create cooperative agent teams from a single prompt.
- 📡 **Connectors Galore**: Built-in integrations with Discord, Slack, Telegram, GitHub Issues, and IRC.
- 📡 **Connectors**: Built-in integrations with Discord, Slack, Telegram, GitHub Issues, and IRC.
- 🛠 **Comprehensive REST API**: Seamless integration into your workflows. Every agent created will support OpenAI Responses API out of the box.
- 📚 **Short & Long-Term Memory**: Powered by [LocalRecall](https://github.com/mudler/LocalRecall).
- 🧠 **Planning & Reasoning**: Agents intelligently plan, reason, and adapt.
@@ -173,6 +173,9 @@ MODEL_NAME=gemma-3-12b-it \
MULTIMODAL_MODEL=moondream2-20250414 \
IMAGE_MODEL=sd-1.5-ggml \
docker compose -f docker-compose.intel.yaml up
# With custom actions directory
LOCALAGI_CUSTOM_ACTIONS_DIR=/app/custom-actions docker compose up
```
If no models are specified, it will use the defaults:
@@ -237,6 +240,7 @@ LocalAGI supports environment configurations. Note that these environment variab
| `LOCALAGI_LOCALRAG_URL` | LocalRecall connection |
| `LOCALAGI_ENABLE_CONVERSATIONS_LOGGING` | Toggle conversation logs |
| `LOCALAGI_API_KEYS` | A comma separated list of api keys used for authentication |
| `LOCALAGI_CUSTOM_ACTIONS_DIR` | Directory containing custom Go action files to be automatically loaded |
## Installation Options
@@ -339,7 +343,6 @@ pool, err := state.NewAgentPool(
"http://localhost:8080", // API URL
"your-api-key", // API key
"./state", // state directory
"", // MCP box URL (optional)
"http://localhost:8081", // LocalRAG API URL
func(config *AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action {
// Define available actions for agents
@@ -425,6 +428,30 @@ LocalAGI provides two powerful ways to extend its functionality with custom acti
LocalAGI supports custom actions written in Go that can be defined inline when creating an agent. These actions are interpreted at runtime, so no compilation is required.
#### Automatic Custom Actions Loading
You can also place custom Go action files in a directory and have LocalAGI automatically load them. Set the `LOCALAGI_CUSTOM_ACTIONS_DIR` environment variable to point to a directory containing your custom action files. Each `.go` file in this directory will be automatically loaded and made available to all agents.
**Example setup:**
```bash
# Set the environment variable
export LOCALAGI_CUSTOM_ACTIONS_DIR="/path/to/custom/actions"
# Or in docker-compose.yaml
environment:
- LOCALAGI_CUSTOM_ACTIONS_DIR=/app/custom-actions
```
**Directory structure:**
```
custom-actions/
├── weather_action.go
├── file_processor.go
└── database_query.go
```
Each file should contain the three required functions (`Run`, `Definition`, `RequiredFields`) as described below.
#### How Custom Actions Work
When creating a new Agent, in the action sections select the "custom" action, you can add the Golang code directly there.
@@ -675,15 +702,39 @@ The development workflow is similar to the source build, but with additional ste
git clone https://github.com/mudler/LocalAGI.git
cd LocalAGI
# Install dependencies and start frontend development server
cd webui/react-ui && bun i && bun run dev
cd webui/react-ui
# Install dependencies
bun i
# Compile frontend (the build directory needs to exist for the backend to start)
bun run build
# Start frontend development server
bun run dev
```
Then in separate terminal:
```bash
cd LocalAGI
# Create a "pool" directory for agent state
mkdir pool
# Set required environment variables
export LOCALAGI_MODEL=gemma-3-4b-it-qat
export LOCALAGI_MULTIMODAL_MODEL=moondream2-20250414
export LOCALAGI_IMAGE_MODEL=sd-1.5-ggml
export LOCALAGI_LLM_API_URL=http://localai:8080
export LOCALAGI_LOCALRAG_URL=http://localrecall:8080
export LOCALAGI_STATE_DIR=./pool
export LOCALAGI_TIMEOUT=5m
export LOCALAGI_ENABLE_CONVERSATIONS_LOGGING=false
export LOCALAGI_SSHBOX_URL=root:root@sshbox:22
# Start development server
cd ../.. && go run main.go
go run main.go
```
> Note: see webui/react-ui/.vite.config.js for env vars that can be used to configure the backend URL
@@ -982,9 +1033,9 @@ LocalAGI supports environment configurations. Note that these environment variab
| `LOCALAGI_STATE_DIR` | Where state gets stored |
| `LOCALAGI_LOCALRAG_URL` | LocalRecall connection |
| `LOCALAGI_SSHBOX_URL` | LocalAGI SSHBox URL, e.g. user:pass@ip:port |
| `LOCALAGI_MCPBOX_URL` | LocalAGI MCPBox URL, e.g. http://mcpbox:8080 |
| `LOCALAGI_ENABLE_CONVERSATIONS_LOGGING` | Toggle conversation logs |
| `LOCALAGI_API_KEYS` | A comma separated list of api keys used for authentication |
| `LOCALAGI_CUSTOM_ACTIONS_DIR` | Directory containing custom Go action files to be automatically loaded |
</details>
## LICENSE
-38
View File
@@ -1,38 +0,0 @@
package main
import (
"flag"
"log"
"os"
"os/signal"
"syscall"
"github.com/mudler/LocalAGI/pkg/stdio"
)
func main() {
// Parse command line flags
addr := flag.String("addr", ":8080", "HTTP server address")
flag.Parse()
// Create and start the server
server := stdio.NewServer()
// Handle graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
log.Printf("Starting server on %s", *addr)
if err := server.Start(*addr); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}()
// Wait for shutdown signal
<-sigChan
log.Println("Shutting down server...")
// TODO: Implement graceful shutdown if needed
os.Exit(0)
}
+42 -5
View File
@@ -3,6 +3,8 @@ package action
import (
"context"
"fmt"
"os"
"regexp"
"strings"
"github.com/mudler/LocalAGI/core/types"
@@ -24,7 +26,7 @@ func NewCustom(config map[string]string, goPkgPath string) (*CustomAction, error
}
if err := a.callInit(); err != nil {
xlog.Error("Error calling custom action init", "error", err)
xlog.Warn("No init function found for custom action", "error", err, "action", a.config["name"])
}
return a, nil
@@ -43,18 +45,23 @@ func (a *CustomAction) callInit() error {
v, err := a.i.Eval(fmt.Sprintf("%s.Init", a.config["name"]))
if err != nil {
return err
xlog.Warn("No init function found for custom action", "error", err, "action", a.config["name"])
return nil
}
run := v.Interface().(func() error)
run, ok := v.Interface().(func(string) error)
if !ok {
return nil
}
return run()
return run(a.config["configuration"])
}
func (a *CustomAction) initializeInterpreter() error {
if _, exists := a.config["code"]; exists && a.i == nil {
unsafe := strings.ToLower(a.config["unsafe"]) == "true"
i := interp.New(interp.Options{
Env: os.Environ(),
GoPath: a.goPkgPath,
Unrestricted: unsafe,
})
@@ -66,6 +73,15 @@ func (a *CustomAction) initializeInterpreter() error {
a.config["name"] = "custom"
}
// let's find first if there is already a package declarated in the code
// the user might want to specify it to not break syntax with IDEs
re := regexp.MustCompile("package (\\w+)")
packageName := re.FindStringSubmatch(a.config["code"])
if len(packageName) > 1 {
// remove it from the code, normalize to `name`
a.config["code"] = re.ReplaceAllString(a.config["code"], "")
}
_, err := i.Eval(fmt.Sprintf("package %s\n%s", a.config["name"], a.config["code"]))
if err != nil {
return err
@@ -106,6 +122,21 @@ func (a *CustomAction) Definition() types.ActionDefinition {
return types.ActionDefinition{}
}
description := ""
desc, err := a.i.Eval(fmt.Sprintf("%s.Description", a.config["name"]))
if err != nil {
xlog.Warn("No description found for custom action", "error", err, "action", a.config["name"])
} else {
d, ok := desc.Interface().(func() string)
if ok {
description = d()
}
}
if a.config["description"] != "" {
description = a.config["description"]
}
properties := v.Interface().(func() map[string][]string)
v, err = a.i.Eval(fmt.Sprintf("%s.RequiredFields", a.config["name"]))
@@ -130,7 +161,7 @@ func (a *CustomAction) Definition() types.ActionDefinition {
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(a.config["name"]),
Description: a.config["description"],
Description: description,
Properties: prop,
Required: requiredFields(),
}
@@ -164,5 +195,11 @@ func CustomConfigMeta() []config.Field {
Type: config.FieldTypeCheckbox,
HelpText: "Allow unsafe code execution",
},
{
Name: "configuration",
Label: "Configuration",
Type: config.FieldTypeTextarea,
HelpText: "Configuration for the custom action",
},
}
}
-48
View File
@@ -1,48 +0,0 @@
package action
import (
"context"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai/jsonschema"
)
// NewGoal creates a new intention action
// The inention action is special as it tries to identify
// a tool to use and a reasoning over to use it
func NewGoal() *GoalAction {
return &GoalAction{}
}
type GoalAction struct {
}
type GoalResponse struct {
Goal string `json:"goal"`
Achieved bool `json:"achieved"`
}
func (a *GoalAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil
}
func (a *GoalAction) Plannable() bool {
return false
}
func (a *GoalAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: "goal",
Description: "Check if the goal is achieved",
Properties: map[string]jsonschema.Definition{
"goal": {
Type: jsonschema.String,
Description: "The goal to check if it is achieved.",
},
"achieved": {
Type: jsonschema.Boolean,
Description: "Whether the goal is achieved",
},
},
Required: []string{"goal", "achieved"},
}
}
-50
View File
@@ -1,50 +0,0 @@
package action
import (
"context"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai/jsonschema"
)
// NewIntention creates a new intention action
// The inention action is special as it tries to identify
// a tool to use and a reasoning over to use it
func NewIntention(s ...string) *IntentAction {
return &IntentAction{tools: s}
}
type IntentAction struct {
tools []string
}
type IntentResponse struct {
Tool string `json:"tool"`
Reasoning string `json:"reasoning"`
}
func (a *IntentAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil
}
func (a *IntentAction) Plannable() bool {
return false
}
func (a *IntentAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: "pick_tool",
Description: "Pick a tool",
Properties: map[string]jsonschema.Definition{
"reasoning": {
Type: jsonschema.String,
Description: "A detailed reasoning on why you want to call this tool.",
},
"tool": {
Type: jsonschema.String,
Description: "The tool you want to use",
Enum: a.tools,
},
},
Required: []string{"tool", "reasoning"},
}
}
-71
View File
@@ -1,71 +0,0 @@
package action
import (
"context"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai/jsonschema"
)
// PlanActionName is the name of the plan action
// used by the LLM to schedule more actions
const PlanActionName = "plan"
func NewPlan(plannableActions []string) *PlanAction {
return &PlanAction{
plannables: plannableActions,
}
}
type PlanAction struct {
plannables []string
}
type PlanResult struct {
Subtasks []PlanSubtask `json:"subtasks"`
Goal string `json:"goal"`
}
type PlanSubtask struct {
Action string `json:"action"`
Reasoning string `json:"reasoning"`
}
func (a *PlanAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil
}
func (a *PlanAction) Plannable() bool {
return false
}
func (a *PlanAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: PlanActionName,
Description: "Use it for situations that involves doing more actions in sequence.",
Properties: map[string]jsonschema.Definition{
"subtasks": {
Type: jsonschema.Array,
Description: "The subtasks to be executed",
Items: &jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"action": {
Type: jsonschema.String,
Description: "The action to call",
Enum: a.plannables,
},
"reasoning": {
Type: jsonschema.String,
Description: "The reasoning for calling this action",
},
},
},
},
"goal": {
Type: jsonschema.String,
Description: "The goal of this plan",
},
},
Required: []string{"subtasks", "goal"},
}
}
-43
View File
@@ -1,43 +0,0 @@
package action
import (
"context"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai/jsonschema"
)
// NewReasoning creates a new reasoning action
// The reasoning action is special as it tries to force the LLM
// to think about what to do next
func NewReasoning() *ReasoningAction {
return &ReasoningAction{}
}
type ReasoningAction struct{}
type ReasoningResponse struct {
Reasoning string `json:"reasoning"`
}
func (a *ReasoningAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil
}
func (a *ReasoningAction) Plannable() bool {
return false
}
func (a *ReasoningAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: "pick_action",
Description: "try to understand what's the best thing to do and pick an action with a reasoning",
Properties: map[string]jsonschema.Definition{
"reasoning": {
Type: jsonschema.String,
Description: "A detailed reasoning on what would you do in this situation.",
},
},
Required: []string{"reasoning"},
}
}
-45
View File
@@ -1,45 +0,0 @@
package action
import (
"context"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai/jsonschema"
)
// ReplyActionName is the name of the reply action
// used by the LLM to reply to the user without
// any additional processing
const ReplyActionName = "reply"
func NewReply() *ReplyAction {
return &ReplyAction{}
}
type ReplyAction struct{}
type ReplyResponse struct {
Message string `json:"message"`
}
func (a *ReplyAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (string, error) {
return "no-op", nil
}
func (a *ReplyAction) Plannable() bool {
return false
}
func (a *ReplyAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: ReplyActionName,
Description: "Use this tool to reply to the user once we have all the informations we need.",
Properties: map[string]jsonschema.Definition{
"message": {
Type: jsonschema.String,
Description: "The message to reply with",
},
},
Required: []string{"message"},
}
}
+8 -506
View File
@@ -1,155 +1,18 @@
package agent
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/mudler/LocalAGI/core/action"
"github.com/mudler/LocalAGI/core/types"
"golang.org/x/exp/slices"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
)
const parameterReasoningPrompt = `You are tasked with generating the optimal parameters for the action "%s". The action requires the following parameters:
%s
Your task is to:
1. Generate the best possible values for each required parameter
2. If the parameter requires code, provide complete, working code
3. If the parameter requires text or documentation, provide comprehensive, well-structured content
4. Ensure all parameters are complete and ready to be used
Focus on quality and completeness. Do not explain your reasoning or analyze the action's purpose - just provide the best possible parameter values.`
type decisionResult struct {
actionParams types.ActionParams
message string
actionName string
}
// decision forces the agent to take one of the available actions
func (a *Agent) decision(
job *types.Job,
conversation []openai.ChatCompletionMessage,
tools []openai.Tool, toolchoice string, maxRetries int) (*decisionResult, error) {
var choice *openai.ToolChoice
if toolchoice != "" {
choice = &openai.ToolChoice{
Type: openai.ToolTypeFunction,
Function: openai.ToolFunction{Name: toolchoice},
}
}
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
for attempts := 0; attempts < maxRetries; attempts++ {
resp, err := a.client.CreateChatCompletion(job.GetContext(), decision)
if err != nil {
lastErr = 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
}
jsonResp, _ := json.Marshal(resp)
xlog.Debug("Decision response", "response", string(jsonResp))
if obs != nil {
obs.AddProgress(types.Progress{
ChatCompletionResponse: &resp,
})
}
if len(resp.Choices) != 1 {
lastErr = fmt.Errorf("no choices: %d", len(resp.Choices))
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
}
msg := resp.Choices[0].Message
if len(msg.ToolCalls) != 1 {
if err := a.saveConversation(append(conversation, msg), "decision"); err != nil {
xlog.Error("Error saving conversation", "error", err)
}
if obs != nil {
obs.MakeLastProgressCompletion()
a.observer.Update(*obs)
}
return &decisionResult{message: msg.Content}, nil
}
params := types.ActionParams{}
if err := params.Read(msg.ToolCalls[0].Function.Arguments); err != nil {
lastErr = 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
}
if err := a.saveConversation(append(conversation, msg), "decision"); err != nil {
xlog.Error("Error saving conversation", "error", err)
}
if obs != nil {
obs.MakeLastProgressCompletion()
a.observer.Update(*obs)
}
return &decisionResult{actionParams: params, actionName: msg.ToolCalls[0].Function.Name, message: msg.Content}, nil
}
return nil, fmt.Errorf("failed to make a decision after %d attempts: %w", maxRetries, lastErr)
}
type Messages []openai.ChatCompletionMessage
func (m Messages) ToOpenAI() []openai.ChatCompletionMessage {
@@ -228,210 +91,11 @@ func (m Messages) GetLatestUserMessage() *openai.ChatCompletionMessage {
return nil
}
func (m Messages) IsLastMessageFromRole(role string) bool {
if len(m) == 0 {
return false
}
return m[len(m)-1].Role == role
}
func (a *Agent) generateParameters(job *types.Job, pickTemplate string, act types.Action, c []openai.ChatCompletionMessage, reasoning string, maxAttempts int) (*decisionResult, error) {
if act == nil {
return nil, fmt.Errorf("action is nil")
}
if len(act.Definition().Properties) > 0 {
xlog.Debug("Action has properties", "action", act.Definition().Name, "properties", act.Definition().Properties)
} else {
xlog.Debug("Action has no properties", "action", act.Definition().Name)
return &decisionResult{actionParams: types.ActionParams{}}, nil
}
stateHUD, err := renderTemplate(pickTemplate, a.prepareHUD(), a.availableActions(), reasoning)
if err != nil {
return nil, err
}
conversation := c
if !Messages(c).Exist(stateHUD) && a.options.enableHUD {
conversation = append([]openai.ChatCompletionMessage{
{
Role: "system",
Content: stateHUD,
},
}, conversation...)
}
cc := conversation
if a.options.forceReasoning {
// First, get the LLM to reason about optimal parameter usage
parameterReasoningPrompt := fmt.Sprintf(parameterReasoningPrompt,
act.Definition().Name,
formatProperties(act.Definition().Properties))
// Get initial reasoning about parameters using askLLM
paramReasoningMsg, err := a.askLLM(job.GetContext(),
append(conversation, openai.ChatCompletionMessage{
Role: "system",
Content: parameterReasoningPrompt,
}),
maxAttempts,
)
if err != nil {
xlog.Warn("Failed to get parameter reasoning", "error", err)
}
// Combine original reasoning with parameter-specific reasoning
enhancedReasoning := reasoning
if paramReasoningMsg.Content != "" {
enhancedReasoning = fmt.Sprintf("%s\n\nParameter Analysis:\n%s", reasoning, paramReasoningMsg.Content)
}
cc = append(conversation, openai.ChatCompletionMessage{
Role: "system",
Content: fmt.Sprintf("The agent decided to use the tool %s with the following reasoning: %s", act.Definition().Name, enhancedReasoning),
})
}
var result *decisionResult
var attemptErr error
for attempts := 0; attempts < maxAttempts; attempts++ {
result, attemptErr = a.decision(job,
cc,
a.availableActions().ToTools(),
act.Definition().Name.String(),
maxAttempts,
)
if attemptErr == nil && result.actionParams != nil {
return result, nil
}
xlog.Warn("Attempt to generate parameters failed", "attempt", attempts+1, "error", attemptErr)
}
return nil, fmt.Errorf("failed to generate parameters after %d attempts: %w", maxAttempts, attemptErr)
}
// Helper function to format properties for the prompt
func formatProperties(props map[string]jsonschema.Definition) string {
var result strings.Builder
for name, prop := range props {
result.WriteString(fmt.Sprintf("- %s: %s\n", name, prop.Description))
}
return result.String()
}
func (a *Agent) handlePlanning(ctx context.Context, job *types.Job, chosenAction types.Action, actionParams types.ActionParams, reasoning string, pickTemplate string, conv Messages) (Messages, error) {
// Planning: run all the actions in sequence
if !chosenAction.Definition().Name.Is(action.PlanActionName) {
xlog.Debug("no plan action")
return conv, nil
}
xlog.Debug("[planning]...")
planResult := action.PlanResult{}
if err := actionParams.Unmarshal(&planResult); err != nil {
return conv, fmt.Errorf("error unmarshalling plan result: %w", err)
}
stateResult := types.ActionState{
ActionCurrentState: types.ActionCurrentState{
Job: job,
Action: chosenAction,
Params: actionParams,
Reasoning: reasoning,
},
ActionResult: types.ActionResult{
Result: fmt.Sprintf("planning %s, subtasks: %+v", planResult.Goal, planResult.Subtasks),
},
}
job.Result.SetResult(stateResult)
job.CallbackWithResult(stateResult)
xlog.Info("[Planning] starts", "agent", a.Character.Name, "goal", planResult.Goal)
for _, s := range planResult.Subtasks {
xlog.Info("[Planning] subtask", "agent", a.Character.Name, "action", s.Action, "reasoning", s.Reasoning)
}
if len(planResult.Subtasks) == 0 {
return conv, fmt.Errorf("no subtasks")
}
// Execute all subtasks in sequence
for _, subtask := range planResult.Subtasks {
xlog.Info("[subtask] Generating parameters",
"agent", a.Character.Name,
"action", subtask.Action,
"reasoning", reasoning,
)
subTaskAction := a.availableActions().Find(subtask.Action)
if subTaskAction == nil {
xlog.Error("Action not found: %s", subtask.Action)
return conv, fmt.Errorf("action %s not found", subtask.Action)
}
subTaskReasoning := fmt.Sprintf("%s Overall goal is: %s", subtask.Reasoning, planResult.Goal)
params, err := a.generateParameters(job, pickTemplate, subTaskAction, conv, subTaskReasoning, maxRetries)
if err != nil {
xlog.Error("error generating action's parameters", "error", err)
return conv, fmt.Errorf("error generating action's parameters: %w", err)
}
actionParams = params.actionParams
if !job.Callback(types.ActionCurrentState{
Job: job,
Action: subTaskAction,
Params: actionParams,
Reasoning: subTaskReasoning,
}) {
job.Result.SetResult(types.ActionState{
ActionCurrentState: types.ActionCurrentState{
Job: job,
Action: chosenAction,
Params: actionParams,
Reasoning: subTaskReasoning,
},
ActionResult: types.ActionResult{
Result: "stopped by callback",
},
})
job.Result.Conversation = conv
job.Result.Finish(nil)
break
}
result, err := a.runAction(job, subTaskAction, actionParams)
if err != nil {
xlog.Error("error running action", "error", err)
return conv, fmt.Errorf("error running action: %w", err)
}
stateResult := types.ActionState{
ActionCurrentState: types.ActionCurrentState{
Job: job,
Action: subTaskAction,
Params: actionParams,
Reasoning: subTaskReasoning,
},
ActionResult: result,
}
job.Result.SetResult(stateResult)
job.CallbackWithResult(stateResult)
xlog.Debug("[subtask] Action executed", "agent", a.Character.Name, "action", subTaskAction.Definition().Name, "result", result)
conv = a.addFunctionResultToConversation(subTaskAction, actionParams, result, conv)
}
return conv, nil
}
// getAvailableActionsForJob returns available actions including user-defined ones for a specific job
func (a *Agent) getAvailableActionsForJob(job *types.Job) types.Actions {
// Start with regular available actions
baseActions := a.availableActions()
// Add user-defined actions from the job
userTools := job.GetUserTools()
if len(userTools) > 0 {
@@ -439,29 +103,14 @@ func (a *Agent) getAvailableActionsForJob(job *types.Job) types.Actions {
baseActions = append(baseActions, userDefinedActions...)
xlog.Debug("Added user-defined actions", "definitions", userTools)
}
return baseActions
}
func (a *Agent) availableActions() types.Actions {
// defaultActions := append(a.options.userActions, action.NewReply())
addPlanAction := func(actions types.Actions) types.Actions {
if !a.options.canPlan {
return actions
}
plannablesActions := []string{}
for _, a := range actions {
if a.Plannable() {
plannablesActions = append(plannablesActions, a.Definition().Name.String())
}
}
planAction := action.NewPlan(plannablesActions)
actions = append(actions, planAction)
return actions
}
defaultActions := append(a.mcpActions, a.options.userActions...)
defaultActions := slices.Clone(a.options.userActions)
if a.options.initiateConversations && a.selfEvaluationInProgress { // && self-evaluation..
acts := append(defaultActions, action.NewConversation())
@@ -472,7 +121,7 @@ func (a *Agent) availableActions() types.Actions {
// acts = append(acts, action.NewStop())
// }
return addPlanAction(acts)
return acts
}
if a.options.canStopItself {
@@ -480,14 +129,14 @@ func (a *Agent) availableActions() types.Actions {
if a.options.enableHUD {
acts = append(acts, action.NewState())
}
return addPlanAction(acts)
return acts
}
if a.options.enableHUD {
return addPlanAction(append(defaultActions, action.NewState()))
return append(defaultActions, action.NewState())
}
return addPlanAction(defaultActions)
return defaultActions
}
func (a *Agent) prepareHUD() (promptHUD *PromptHUD) {
@@ -502,150 +151,3 @@ func (a *Agent) prepareHUD() (promptHUD *PromptHUD) {
ShowCharacter: a.options.showCharacter,
}
}
// pickAction picks an action based on the conversation
func (a *Agent) pickAction(job *types.Job, templ string, messages []openai.ChatCompletionMessage, maxRetries int) (types.Action, types.ActionParams, string, error) {
c := messages
xlog.Debug("[pickAction] picking action starts", "messages", messages)
// Get available actions including user-defined ones
availableActions := a.getAvailableActionsForJob(job)
// Identify the goal of this conversation
if !a.options.forceReasoning || job.ToolChoice != "" {
xlog.Debug("not forcing reasoning", "forceReasoning", a.options.forceReasoning, "ToolChoice", job.ToolChoice)
// 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
thought, err := a.decision(job,
messages,
availableActions.ToTools(),
job.ToolChoice,
maxRetries)
if err != nil {
return nil, nil, "", err
}
xlog.Debug("thought action Name", "actionName", thought.actionName)
xlog.Debug("thought message", "message", thought.message)
// Find the action
chosenAction := availableActions.Find(thought.actionName)
if chosenAction == nil || thought.actionName == "" {
xlog.Debug("no answer")
// LLM replied with an answer?
//fmt.Errorf("no action found for intent:" + thought.actioName)
return nil, nil, thought.message, nil
}
xlog.Debug(fmt.Sprintf("chosenAction: %v", chosenAction.Definition().Name))
return chosenAction, thought.actionParams, thought.message, nil
}
// Force the LLM to think and we extract a "reasoning" to pick a specific action and with which parameters
xlog.Debug("[pickAction] forcing reasoning")
prompt, err := renderTemplate(templ, a.prepareHUD(), a.availableActions(), "")
if err != nil {
return nil, nil, "", err
}
// Get the LLM to think on what to do
// and have a thought
if !Messages(c).Exist(prompt) {
c = append([]openai.ChatCompletionMessage{
{
Role: "system",
Content: prompt,
},
}, c...)
}
// Create a detailed prompt for reasoning that includes available actions and their properties
reasoningPrompt := "Analyze the current situation and determine the best course of action. Consider the following:\n\n"
reasoningPrompt += "Available Actions:\n"
for _, act := range a.availableActions() {
reasoningPrompt += fmt.Sprintf("- %s: %s\n", act.Definition().Name, act.Definition().Description)
if len(act.Definition().Properties) > 0 {
reasoningPrompt += " Properties:\n"
for name, prop := range act.Definition().Properties {
reasoningPrompt += fmt.Sprintf(" - %s: %s\n", name, prop.Description)
}
}
reasoningPrompt += "\n"
}
reasoningPrompt += "\nProvide a detailed reasoning about what action would be most appropriate in this situation and why. You can also just reply with a simple message by choosing the 'reply' or 'answer' action."
// Get reasoning using askLLM
reasoningMsg, err := a.askLLM(job.GetContext(),
append(c, openai.ChatCompletionMessage{
Role: "system",
Content: reasoningPrompt,
}),
maxRetries)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to get reasoning: %w", err)
}
originalReasoning := reasoningMsg.Content
xlog.Debug("[pickAction] picking action", "messages", c)
actionsID := []string{"reply"}
for _, m := range a.availableActions() {
actionsID = append(actionsID, m.Definition().Name.String())
}
xlog.Debug("[pickAction] actionsID", "actionsID", actionsID)
intentionsTools := action.NewIntention(actionsID...)
// TODO: FORCE to select ana ction here
// NOTE: we do not give the full conversation here to pick the action
// to avoid hallucinations
// Extract an action
params, err := a.decision(job,
append(c, openai.ChatCompletionMessage{
Role: "system",
Content: "Pick the relevant action given the following reasoning: " + originalReasoning,
}),
types.Actions{intentionsTools}.ToTools(),
intentionsTools.Definition().Name.String(), maxRetries)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to get the action tool parameters: %v", err)
}
if params.actionParams == nil {
xlog.Debug("[pickAction] no action params found")
return nil, nil, params.message, nil
}
actionChoice := action.IntentResponse{}
err = params.actionParams.Unmarshal(&actionChoice)
if err != nil {
return nil, nil, "", err
}
if actionChoice.Tool == "" || actionChoice.Tool == "reply" {
xlog.Debug("[pickAction] no action found, replying")
return nil, nil, "", nil
}
chosenAction := a.availableActions().Find(actionChoice.Tool)
xlog.Debug("[pickAction] chosenAction", "chosenAction", chosenAction, "actionName", actionChoice.Tool)
// // Let's double check if the action is correct by asking the LLM to judge it
// if chosenAction!= nil {
// promptString:= "Given the following goal and thoughts, is the action correct? \n\n"
// promptString+= fmt.Sprintf("Goal: %s\n", goalResponse.Goal)
// promptString+= fmt.Sprintf("Thoughts: %s\n", originalReasoning)
// promptString+= fmt.Sprintf("Action: %s\n", chosenAction.Definition().Name.String())
// promptString+= fmt.Sprintf("Action description: %s\n", chosenAction.Definition().Description)
// promptString+= fmt.Sprintf("Action parameters: %s\n", params.actionParams)
// }
return chosenAction, nil, originalReasoning, nil
}
+486 -516
View File
File diff suppressed because it is too large Load Diff
+44 -15
View File
@@ -37,7 +37,8 @@ var debugOptions = []types.JobOption{
}
type TestAction struct {
response map[string]string
response map[string]string
definition *types.ActionDefinition
}
func (a *TestAction) Plannable() bool {
@@ -55,7 +56,7 @@ func (a *TestAction) Run(c context.Context, sharedState *types.AgentSharedState,
}
func (a *TestAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
def := types.ActionDefinition{
Name: "get_weather",
Description: "get current weather",
Properties: map[string]jsonschema.Definition{
@@ -71,6 +72,11 @@ func (a *TestAction) Definition() types.ActionDefinition {
Required: []string{"location"},
}
if a.definition != nil {
def = *a.definition
}
return def
}
type FakeStoreResultAction struct {
@@ -128,7 +134,6 @@ var _ = Describe("Agent test", func() {
WithModel(testModel),
EnableForceReasoning,
WithTimeout("10m"),
WithLoopDetectionSteps(3),
// WithRandomIdentity(),
WithActions(&TestAction{response: map[string]string{
"boston": testActionResult,
@@ -153,6 +158,9 @@ var _ = Describe("Agent test", func() {
}
Expect(reasons).To(ContainElement(testActionResult), fmt.Sprint(res))
Expect(reasons).To(ContainElement(testActionResult2), fmt.Sprint(res))
Expect(len(res.Conversation)).To(BeNumerically(">", 1), fmt.Sprint(res.Conversation))
reasons = []string{}
res = agent.Ask(
@@ -225,11 +233,33 @@ var _ = Describe("Agent test", func() {
WithModel(testModel),
WithLLMAPIKey(apiKeyURL),
WithTimeout("10m"),
WithMaxEvaluationLoops(1),
WithActions(
&TestAction{response: map[string]string{
"boston": testActionResult,
"milan": testActionResult2,
}},
&TestAction{
response: map[string]string{
"boston": testActionResult,
"milan": testActionResult2,
},
},
&TestAction{
response: map[string]string{
"flight": "Flight options from Boston to Milan (April 22-26, 2025):\n• Outbound: Boston Logan (BOS) → Milan Malpensa (MXP), April 22, 2025\n - Economy: $450-650 (Alitalia, Delta, Lufthansa)\n - Business: $1,200-1,800\n - Flight time: 8h 15m (1 stop) or 9h 45m (direct)\n• Return: Milan Malpensa (MXP) → Boston Logan (BOS), April 26, 2025\n - Economy: $420-580\n - Business: $1,100-1,600\n• Total estimated cost: $870-1,230 per person\n• Best booking window: 2-3 months in advance for optimal prices",
"hotel": "Hotel recommendations in Milan for April 22-26, 2025:\n• Luxury (4-5 stars): $200-400/night\n - Hotel Principe di Savoia: $380/night (central location)\n - Mandarin Oriental: $420/night (luxury amenities)\n• Mid-range (3-4 stars): $120-200/night\n - Hotel Spadari al Duomo: $160/night (near cathedral)\n - Hotel Milano Scala: $140/night (theater district)\n• Budget (2-3 stars): $80-120/night\n - Hotel Bernina: $95/night (near train station)\n• Total 4-night stay: $320-1,680 depending on category\n• Booking tip: Reserve early for spring season discounts",
"car": "Car rental options in Milan for April 22-26, 2025:\n• Economy cars: $35-50/day (Fiat 500, VW Polo)\n• Compact cars: $45-65/day (Ford Focus, Opel Astra)\n• Mid-size cars: $60-85/day (BMW 3 Series, Audi A4)\n• SUV/Luxury: $90-150/day (BMW X3, Mercedes E-Class)\n• Total 4-day rental: $140-600\n• Pickup locations: Milan Malpensa Airport, Milan Central Station, city center\n• Insurance: $15-25/day additional\n• Fuel: ~$60-80 for 4 days of city driving\n• Parking: $20-40/day in city center hotels",
"food": "Dining budget and recommendations for Milan (April 22-26, 2025):\n• Fine dining: $80-150/person (Michelin-starred restaurants)\n - Cracco: $120/person (2 Michelin stars)\n - Trussardi alla Scala: $100/person\n• Mid-range restaurants: $40-80/person\n - Luini: $15/person (famous panzerotti)\n - Piz: $25/person (authentic pizza)\n• Casual dining: $20-40/person\n - Aperitivo bars: $15-25/person\n - Street food: $8-15/person\n• Daily food budget: $60-120/person\n• Total 4-day food cost: $240-480/person\n• Must-try: Risotto alla Milanese, Osso Buco, Panettone",
},
definition: &types.ActionDefinition{
Name: "search_internet",
Description: "search the internet for information",
Properties: map[string]jsonschema.Definition{
"query": {
Type: jsonschema.String,
Description: "The query to search for",
},
},
Required: []string{"query"},
},
},
),
EnablePlanning,
EnableForceReasoning,
@@ -241,21 +271,20 @@ var _ = Describe("Agent test", func() {
defer agent.Stop()
result := agent.Ask(
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"),
types.WithText("Create a plan for my 4-day trip from Boston to milan in April of this year (2025). I'm not sure about the dates yet, I want you to find out the best dates also according to what you find."),
)
Expect(len(result.State)).To(BeNumerically(">", 1))
Expect(len(result.Conversation)).To(BeNumerically(">", 1), fmt.Sprint(result.Conversation))
Expect(len(result.Plans)).To(BeNumerically(">=", 1), fmt.Sprintf("%+v", result))
Expect(len(result.State)).To(BeNumerically(">=", 1))
actionsExecuted := []string{}
actionResults := []string{}
for _, r := range result.State {
xlog.Info(r.Result)
actionsExecuted = append(actionsExecuted, r.Action.Definition().Name.String())
actionResults = append(actionResults, r.ActionResult.Result)
}
Expect(actionsExecuted).To(ContainElement("get_weather"), 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))
Expect(actionsExecuted).To(Or(ContainElement("search_internet"), ContainElement("get_weather")), fmt.Sprint(result))
})
It("Can initiate conversations", func() {
-162
View File
@@ -1,162 +0,0 @@
package agent
import (
"fmt"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/llm"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
)
type EvaluationResult struct {
Satisfied bool `json:"satisfied"`
Gaps []string `json:"gaps"`
Reasoning string `json:"reasoning"`
}
type GoalExtraction struct {
Goal string `json:"goal"`
Constraints []string `json:"constraints"`
Context string `json:"context"`
}
func (a *Agent) extractGoal(job *types.Job, conv []openai.ChatCompletionMessage) (*GoalExtraction, error) {
// Create the goal extraction schema
schema := jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"goal": {
Type: jsonschema.String,
Description: "The main goal or request from the user",
},
"constraints": {
Type: jsonschema.Array,
Items: &jsonschema.Definition{
Type: jsonschema.String,
},
Description: "Any constraints or requirements specified by the user",
},
"context": {
Type: jsonschema.String,
Description: "Additional context that might be relevant for understanding the goal",
},
},
Required: []string{"goal", "constraints", "context"},
}
// Create the goal extraction prompt
prompt := `Analyze the conversation and extract the user's main goal, any constraints, and relevant context.
Consider the entire conversation history to understand the complete context and requirements.
Focus on identifying the primary objective and any specific requirements or limitations mentioned.`
var result GoalExtraction
err := llm.GenerateTypedJSONWithConversation(job.GetContext(), a.client,
append(
[]openai.ChatCompletionMessage{
{
Role: "system",
Content: prompt,
},
},
conv...), a.options.LLMAPI.Model, schema, &result)
if err != nil {
return nil, fmt.Errorf("error extracting goal: %w", err)
}
return &result, nil
}
func (a *Agent) evaluateJob(job *types.Job, conv []openai.ChatCompletionMessage) (*EvaluationResult, error) {
if !a.options.enableEvaluation {
return &EvaluationResult{Satisfied: true}, nil
}
// Extract the goal first
goal, err := a.extractGoal(job, conv)
if err != nil {
return nil, fmt.Errorf("error extracting goal: %w", err)
}
// Create the evaluation schema
schema := jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"satisfied": {
Type: jsonschema.Boolean,
},
"gaps": {
Type: jsonschema.Array,
Items: &jsonschema.Definition{
Type: jsonschema.String,
},
},
"reasoning": {
Type: jsonschema.String,
},
},
Required: []string{"satisfied", "gaps", "reasoning"},
}
// Create the evaluation prompt
prompt := fmt.Sprintf(`Evaluate if the assistant has satisfied the user's request. Consider:
1. The identified goal: %s
2. Constraints and requirements: %v
3. Context: %s
4. The conversation history
5. Any gaps or missing information
6. Whether the response fully addresses the user's needs
Provide a detailed evaluation with specific gaps if any are found.`,
goal.Goal,
goal.Constraints,
goal.Context)
var result EvaluationResult
err = llm.GenerateTypedJSONWithConversation(job.GetContext(), a.client,
append(
[]openai.ChatCompletionMessage{
{
Role: "system",
Content: prompt,
},
},
conv...),
a.options.LLMAPI.Model, schema, &result)
if err != nil {
return nil, fmt.Errorf("error generating evaluation: %w", err)
}
return &result, nil
}
func (a *Agent) handleEvaluation(job *types.Job, conv []openai.ChatCompletionMessage, currentLoop int) (bool, []openai.ChatCompletionMessage, error) {
if !a.options.enableEvaluation || currentLoop >= a.options.maxEvaluationLoops {
return true, conv, nil
}
result, err := a.evaluateJob(job, conv)
if err != nil {
return false, conv, err
}
if result.Satisfied {
return true, conv, nil
}
// If there are gaps, we need to address them
if len(result.Gaps) > 0 {
// Add the evaluation result to the conversation
conv = append(conv, openai.ChatCompletionMessage{
Role: "system",
Content: fmt.Sprintf("Evaluation found gaps that need to be addressed:\n%s\nReasoning: %s",
result.Gaps, result.Reasoning),
})
xlog.Debug("Evaluation found gaps, incrementing loop count", "loop", currentLoop+1)
return false, conv, nil
}
return true, conv, nil
}
+6 -6
View File
@@ -8,6 +8,7 @@ import (
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/cogito"
"github.com/sashabaranov/go-openai"
)
@@ -17,7 +18,7 @@ func (a *Agent) knowledgeBaseLookup(job *types.Job, conv Messages) Messages {
xlog.Debug("[Knowledge Base Lookup] Disabled, skipping", "agent", a.Character.Name)
return conv
}
var obs *types.Observable
if job != nil && job.Obs != nil && a.observer != nil {
obs = a.observer.NewObservable()
@@ -90,7 +91,7 @@ func (a *Agent) knowledgeBaseLookup(job *types.Job, conv Messages) Messages {
if obs != nil {
obs.Completion = &types.Completion{
Conversation: []openai.ChatCompletionMessage{systemMessage},
Conversation: []openai.ChatCompletionMessage{systemMessage},
}
a.observer.Update(*obs)
}
@@ -125,13 +126,12 @@ func (a *Agent) saveCurrentConversation(conv Messages) {
xlog.Info("Saving conversation", "agent", a.Character.Name, "conversation size", len(conv))
if a.options.enableSummaryMemory && len(conv) > 0 {
msg, err := a.askLLM(a.context.Context, []openai.ChatCompletionMessage{{
Role: "user",
Content: "Summarize the conversation below, keep the highlights as a bullet list:\n" + Messages(conv).String(),
}}, maxRetries)
fragment := cogito.NewEmptyFragment().AddStartMessage("user", "Summarize the conversation below, keep the highlights as a bullet list:\n"+Messages(conv).String())
fragment, err := a.llm.Ask(a.context.Context, fragment)
if err != nil {
xlog.Error("Error summarizing conversation", "error", err)
}
msg := fragment.LastMessage()
if err := a.options.ragdb.Store(msg.Content); err != nil {
xlog.Error("Error storing into memory", "error", err)
+110 -114
View File
@@ -3,18 +3,21 @@ package agent
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"time"
mcp "github.com/metoro-io/mcp-golang"
"github.com/metoro-io/mcp-golang/transport/http"
stdioTransport "github.com/metoro-io/mcp-golang/transport/stdio"
"net/http"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/stdio"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/sashabaranov/go-openai/jsonschema"
)
var _ types.Action = &mcpAction{}
var _ types.Action = &mcpWrapperAction{}
type MCPServer struct {
URL string `json:"url"`
@@ -27,44 +30,20 @@ type MCPSTDIOServer struct {
Cmd string `json:"cmd"`
}
type mcpAction struct {
mcpClient *mcp.Client
type mcpWrapperAction struct {
mcpClient *mcp.ClientSession
inputSchema ToolInputSchema
toolName string
toolDescription string
}
func (a *mcpAction) Plannable() bool {
return true
func (m *mcpWrapperAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
// We don't call the method here, it is used by cogito.
// We will just use these to have a list of actions that MCP server provides for resolving internal states
return types.ActionResult{Result: "MCP action called"}, fmt.Errorf("not implemented")
}
func (m *mcpAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
resp, err := m.mcpClient.CallTool(ctx, m.toolName, params)
if err != nil {
xlog.Error("Failed to call tool", "error", err.Error())
return types.ActionResult{}, err
}
xlog.Debug("MCP response", "response", resp)
textResult := ""
for _, c := range resp.Content {
switch c.Type {
case mcp.ContentTypeText:
textResult += c.TextContent.Text + "\n"
case mcp.ContentTypeImage:
xlog.Error("Image content not supported yet")
case mcp.ContentTypeEmbeddedResource:
xlog.Error("Embedded resource content not supported yet")
}
}
return types.ActionResult{
Result: textResult,
}, nil
}
func (m *mcpAction) Definition() types.ActionDefinition {
func (m *mcpWrapperAction) Definition() types.ActionDefinition {
props := map[string]jsonschema.Definition{}
dat, err := json.Marshal(m.inputSchema.Properties)
if err != nil {
@@ -87,87 +66,106 @@ type ToolInputSchema struct {
Required []string `json:"required,omitempty"`
}
func (a *Agent) addTools(client *mcp.Client) (types.Actions, error) {
func (a *Agent) addTools(client *mcp.ClientSession) (types.Actions, error) {
var generatedActions types.Actions
xlog.Debug("Initializing client")
// Initialize the client
response, e := client.Initialize(a.context)
if e != nil {
xlog.Error("Failed to initialize client", "error", e.Error())
return nil, e
tools, err := client.ListTools(a.context, nil)
if err != nil {
xlog.Error("Failed to list tools", "error", err.Error())
return nil, err
}
xlog.Debug("Client initialized: %v", response.Instructions)
for _, t := range tools.Tools {
desc := ""
if t.Description != "" {
desc = t.Description
}
var cursor *string
for {
tools, err := client.ListTools(a.context, cursor)
xlog.Debug("Tool", "name", t.Name, "description", desc)
dat, err := json.Marshal(t.InputSchema)
if err != nil {
xlog.Error("Failed to list tools", "error", err.Error())
return nil, err
xlog.Error("Failed to marshal input schema", "error", err.Error())
}
for _, t := range tools.Tools {
desc := ""
if t.Description != nil {
desc = *t.Description
}
xlog.Debug("Input schema", "tool", t.Name, "schema", string(dat))
xlog.Debug("Tool", "name", t.Name, "description", desc)
dat, err := json.Marshal(t.InputSchema)
if err != nil {
xlog.Error("Failed to marshal input schema", "error", err.Error())
}
xlog.Debug("Input schema", "tool", t.Name, "schema", string(dat))
// XXX: This is a wild guess, to verify (data types might be incompatible)
var inputSchema ToolInputSchema
err = json.Unmarshal(dat, &inputSchema)
if err != nil {
xlog.Error("Failed to unmarshal input schema", "error", err.Error())
}
// Create a new action with Client + tool
generatedActions = append(generatedActions, &mcpAction{
mcpClient: client,
toolName: t.Name,
inputSchema: inputSchema,
toolDescription: desc,
})
// XXX: This is a wild guess, to verify (data types might be incompatible)
var inputSchema ToolInputSchema
err = json.Unmarshal(dat, &inputSchema)
if err != nil {
xlog.Error("Failed to unmarshal input schema", "error", err.Error())
}
if tools.NextCursor == nil {
break // No more pages
}
cursor = tools.NextCursor
// Create a new action with Client + tool
generatedActions = append(generatedActions, &mcpWrapperAction{
mcpClient: client,
toolName: t.Name,
inputSchema: inputSchema,
toolDescription: desc,
})
}
return generatedActions, nil
}
// bearerTokenRoundTripper is a custom roundtripper that injects a bearer token
// into HTTP requests
type bearerTokenRoundTripper struct {
token string
base http.RoundTripper
}
// RoundTrip implements the http.RoundTripper interface
func (rt *bearerTokenRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if rt.token != "" {
req.Header.Set("Authorization", "Bearer "+rt.token)
}
return rt.base.RoundTrip(req)
}
// newBearerTokenRoundTripper creates a new roundtripper that injects the given token
func newBearerTokenRoundTripper(token string, base http.RoundTripper) http.RoundTripper {
if base == nil {
base = http.DefaultTransport
}
return &bearerTokenRoundTripper{
token: token,
base: base,
}
}
func (a *Agent) initMCPActions() error {
a.closeMCPSTDIOServers() // Make sure we stop all previous servers if any is active
a.mcpActions = nil
a.mcpActionDefinitions = nil
var err error
generatedActions := types.Actions{}
client := mcp.NewClient(&mcp.Implementation{Name: "LocalAI", Version: "v1.0.0"}, nil)
// Connect to a server over stdin/stdout.
// MCP HTTP Servers
for _, mcpServer := range a.options.mcpServers {
transport := http.NewHTTPClientTransport("/mcp")
transport.WithBaseURL(mcpServer.URL)
if mcpServer.Token != "" {
transport.WithHeader("Authorization", "Bearer "+mcpServer.Token)
// Create HTTP client with custom roundtripper for bearer token injection
httpclient := &http.Client{
Timeout: 360 * time.Second,
Transport: newBearerTokenRoundTripper(mcpServer.Token, http.DefaultTransport),
}
transport := &mcp.SSEClientTransport{HTTPClient: httpclient, Endpoint: mcpServer.URL}
// Create a new client
client := mcp.NewClient(transport)
session, err := client.Connect(a.context, transport, nil)
if err != nil {
xlog.Error("Failed to connect to MCP server", "server", mcpServer, "error", err.Error())
continue
}
a.mcpSessions = append(a.mcpSessions, session)
xlog.Debug("Adding tools for MCP server", "server", mcpServer)
actions, err := a.addTools(client)
actions, err := a.addTools(session)
if err != nil {
xlog.Error("Failed to add tools for MCP server", "server", mcpServer, "error", err.Error())
}
@@ -179,47 +177,45 @@ func (a *Agent) initMCPActions() error {
a.closeMCPSTDIOServers() // Make sure we stop all previous servers if any is active
if a.options.mcpPrepareScript != "" {
xlog.Debug("Preparing MCP box", "script", a.options.mcpPrepareScript)
client := stdio.NewClient(a.options.mcpBoxURL)
client.RunProcess(a.context, "/bin/bash", []string{"-c", a.options.mcpPrepareScript}, []string{})
xlog.Debug("Preparing MCP", "script", a.options.mcpPrepareScript)
prepareCmd := exec.Command("/bin/bash", "-c", a.options.mcpPrepareScript)
output, err := prepareCmd.CombinedOutput()
if err != nil {
xlog.Error("Failed with error: '%s' - %s", err.Error(), output)
}
xlog.Debug("Prepared MCP: \n%s", output)
}
for _, mcpStdioServer := range a.options.mcpStdioServers {
client := stdio.NewClient(a.options.mcpBoxURL)
p, err := client.CreateProcess(a.context,
mcpStdioServer.Cmd,
mcpStdioServer.Args,
mcpStdioServer.Env,
a.Character.Name)
if err != nil {
xlog.Error("Failed to create process", "error", err.Error())
continue
}
read, writer, err := client.GetProcessIO(p.ID)
if err != nil {
xlog.Error("Failed to get process IO", "error", err.Error())
continue
}
transport := stdioTransport.NewStdioServerTransportWithIO(read, writer)
command := exec.Command(mcpStdioServer.Cmd, mcpStdioServer.Args...)
command.Env = os.Environ()
command.Env = append(command.Env, mcpStdioServer.Env...)
// Create a new client
mcpClient := mcp.NewClient(transport)
session, err := client.Connect(a.context, &mcp.CommandTransport{
Command: command}, nil)
if err != nil {
xlog.Error("Failed to connect to MCP server", "server", mcpStdioServer, "error", err.Error())
continue
}
a.mcpSessions = append(a.mcpSessions, session)
xlog.Debug("Adding tools for MCP server (stdio)", "server", mcpStdioServer)
actions, err := a.addTools(mcpClient)
actions, err := a.addTools(session)
if err != nil {
xlog.Error("Failed to add tools for MCP server", "server", mcpStdioServer, "error", err.Error())
}
generatedActions = append(generatedActions, actions...)
}
a.mcpActions = generatedActions
a.mcpActionDefinitions = generatedActions
return err
}
func (a *Agent) closeMCPSTDIOServers() {
client := stdio.NewClient(a.options.mcpBoxURL)
client.StopGroup(a.Character.Name)
for _, s := range a.mcpSessions {
s.Close()
}
}
+13 -4
View File
@@ -14,6 +14,7 @@ type Observer interface {
NewObservable() *types.Observable
Update(types.Observable)
History() []types.Observable
ClearHistory()
}
type SSEObserver struct {
@@ -21,8 +22,8 @@ type SSEObserver struct {
maxID int32
manager sse.Manager
mutex sync.Mutex
history []types.Observable
mutex sync.Mutex
history []types.Observable
historyLast int
}
@@ -36,8 +37,8 @@ func NewSSEObserver(agent string, manager sse.Manager) *SSEObserver {
}
func (s *SSEObserver) NewObservable() *types.Observable {
id := atomic.AddInt32(&s.maxID, 1)
id := atomic.AddInt32(&s.maxID, 1)
return &types.Observable{
ID: id - 1,
Agent: s.agent,
@@ -86,3 +87,11 @@ func (s *SSEObserver) History() []types.Observable {
return h
}
func (s *SSEObserver) ClearHistory() {
s.mutex.Lock()
defer s.mutex.Unlock()
s.history = make([]types.Observable, 100)
s.historyLast = 0
}
+33 -23
View File
@@ -12,10 +12,13 @@ import (
type Option func(*options) error
type llmOptions struct {
APIURL string
APIKey string
Model string
MultimodalModel string
APIURL string
APIKey string
Model string
MultimodalModel string
TranscriptionModel string
TranscriptionLanguage string
TTSModel string
}
type options struct {
@@ -30,7 +33,6 @@ type options struct {
canStopItself bool
initiateConversations bool
loopDetectionSteps int
forceReasoning bool
canPlan bool
characterfile string
@@ -58,7 +60,6 @@ type options struct {
mcpServers []MCPServer
mcpStdioServers []MCPSTDIOServer
mcpBoxURL string
mcpPrepareScript string
newConversationsSubscribers []func(openai.ChatCompletionMessage)
@@ -76,12 +77,14 @@ func defaultOptions() *options {
return &options{
parallelJobs: 1,
periodicRuns: 15 * time.Minute,
loopDetectionSteps: 10,
maxEvaluationLoops: 2,
enableEvaluation: false,
LLMAPI: llmOptions{
APIURL: "http://localhost:8080",
Model: "gpt-4",
APIURL: "http://localhost:8080",
Model: "gpt-4",
TranscriptionModel: "whisper-1",
TranscriptionLanguage: "",
TTSModel: "tts-1",
},
character: Character{
Name: "",
@@ -131,13 +134,6 @@ func WithTimeout(timeout string) Option {
}
}
func WithLoopDetectionSteps(steps int) Option {
return func(o *options) error {
o.loopDetectionSteps = steps
return nil
}
}
func WithConversationsPath(path string) Option {
return func(o *options) error {
o.conversationsPath = path
@@ -238,13 +234,6 @@ func WithMCPSTDIOServers(servers ...MCPSTDIOServer) Option {
}
}
func WithMCPBoxURL(url string) Option {
return func(o *options) error {
o.mcpBoxURL = url
return nil
}
}
func WithMCPPrepareScript(script string) Option {
return func(o *options) error {
o.mcpPrepareScript = script
@@ -425,3 +414,24 @@ func EnableEvaluation() Option {
return nil
}
}
func WithTranscriptionModel(model string) Option {
return func(o *options) error {
o.LLMAPI.TranscriptionModel = model
return nil
}
}
func WithTranscriptionLanguage(language string) Option {
return func(o *options) error {
o.LLMAPI.TranscriptionLanguage = language
return nil
}
}
func WithTTSModel(model string) Option {
return func(o *options) error {
o.LLMAPI.TTSModel = model
return nil
}
}
+3 -1
View File
@@ -1,6 +1,8 @@
package agent
import "github.com/mudler/LocalAGI/core/types"
type DynamicPrompt interface {
Render(a *Agent) (string, error)
Render(a *Agent) (types.PromptResult, error)
Role() string
}
+19 -44
View File
@@ -2,18 +2,32 @@ package agent
import (
"bytes"
"html/template"
"text/template"
"time"
"github.com/Masterminds/sprig/v3"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai"
)
func templateBase(templateName, templatetext string) (*template.Template, error) {
return template.New(templateName).Funcs(sprig.FuncMap()).Parse(templatetext)
}
func templateExecute(template *template.Template, data interface{}) (string, error) {
prompt := bytes.NewBuffer([]byte{})
err := template.Execute(prompt, data)
if err != nil {
return "", err
}
return prompt.String(), nil
}
func renderTemplate(templ string, hud *PromptHUD, actions types.Actions, reasoning string) (string, error) {
// prepare the prompt
prompt := bytes.NewBuffer([]byte{})
promptTemplate, err := template.New("pickAction").Parse(templ)
promptTemplate, err := templateBase("pickAction", templ)
if err != nil {
return "", err
}
@@ -34,7 +48,7 @@ func renderTemplate(templ string, hud *PromptHUD, actions types.Actions, reasoni
Actions: definitions,
HUD: hud,
Reasoning: reasoning,
Time: time.Now().Format(time.RFC3339),
Time: time.Now().UTC().Format(time.RFC1123),
})
if err != nil {
return "", err
@@ -80,7 +94,8 @@ Current State:
- Current Goal: {{if .CurrentState.Goal}}{{.CurrentState.Goal}}{{else}}None{{end}}
- Action History: {{range .CurrentState.DoneHistory}}{{.}} {{end}}
- Short-term Memory: {{range .CurrentState.Memories}}{{.}} {{end}}{{end}}
Current Time: {{.Time}}`
Current Time and Date: {{.Time}}`
const pickSelfTemplate = `
You are an autonomous AI agent with a defined character and state (as shown above).
@@ -93,7 +108,6 @@ Guidelines:
4. Update your state appropriately
When making decisions:
- Use the "reply" tool to provide final responses
- Update your state using appropriate tools
- Plan complex tasks using the planning tool
- Consider both immediate and long-term goals
@@ -104,43 +118,4 @@ Remember:
- Keep track of your progress and state
- Be proactive in addressing potential issues
Available Tools:
{{range .Actions -}}
- {{.Name}}: {{.Description }}
{{ end }}
{{if .Reasoning}}Previous Reasoning: {{.Reasoning}}{{end}}
` + hudTemplate
const reSelfEvalTemplate = pickSelfTemplate
const pickActionTemplate = hudTemplate + `
Your only task is to analyze the conversation and determine a goal and the best tool to use, or just a final response if we have fullfilled the goal.
Guidelines:
1. Review the current state, what was done already and context
2. Consider available tools and their purposes
3. Plan your approach carefully
4. Explain your reasoning clearly
When choosing actions:
- Use "reply" or "answer" tools for direct responses
- Select appropriate tools for specific tasks
- Consider the impact of each action
- Plan for potential challenges
Decision Process:
1. Analyze the situation
2. Consider available options
3. Choose the best course of action
4. Explain your reasoning
5. Execute the chosen action
Available Tools:
{{range .Actions -}}
- {{.Name}}: {{.Description }}
{{ end }}
{{if .Reasoning}}Previous Reasoning: {{.Reasoning}}{{end}}`
const reEvalTemplate = pickActionTemplate
+32 -19
View File
@@ -43,18 +43,20 @@ type AgentConfig struct {
MCPServers []agent.MCPServer `json:"mcp_servers" form:"mcp_servers"`
MCPSTDIOServers []agent.MCPSTDIOServer `json:"mcp_stdio_servers" form:"mcp_stdio_servers"`
MCPPrepareScript string `json:"mcp_prepare_script" form:"mcp_prepare_script"`
MCPBoxURL string `json:"mcp_box_url" form:"mcp_box_url"`
Filters []FiltersConfig `json:"filters" form:"filters"`
Description string `json:"description" form:"description"`
Model string `json:"model" form:"model"`
MultimodalModel string `json:"multimodal_model" form:"multimodal_model"`
APIURL string `json:"api_url" form:"api_url"`
APIKey string `json:"api_key" form:"api_key"`
LocalRAGURL string `json:"local_rag_url" form:"local_rag_url"`
LocalRAGAPIKey string `json:"local_rag_api_key" form:"local_rag_api_key"`
LastMessageDuration string `json:"last_message_duration" form:"last_message_duration"`
Model string `json:"model" form:"model"`
MultimodalModel string `json:"multimodal_model" form:"multimodal_model"`
TranscriptionModel string `json:"transcription_model" form:"transcription_model"`
TranscriptionLanguage string `json:"transcription_language" form:"transcription_language"`
TTSModel string `json:"tts_model" form:"tts_model"`
APIURL string `json:"api_url" form:"api_url"`
APIKey string `json:"api_key" form:"api_key"`
LocalRAGURL string `json:"local_rag_url" form:"local_rag_url"`
LocalRAGAPIKey string `json:"local_rag_api_key" form:"local_rag_api_key"`
LastMessageDuration string `json:"last_message_duration" form:"last_message_duration"`
Name string `json:"name" form:"name"`
HUD bool `json:"hud" form:"hud"`
@@ -68,7 +70,6 @@ type AgentConfig struct {
EnableKnowledgeBase bool `json:"enable_kb" form:"enable_kb"`
EnableReasoning bool `json:"enable_reasoning" form:"enable_reasoning"`
KnowledgeBaseResults int `json:"kb_results" form:"kb_results"`
LoopDetectionSteps int `json:"loop_detection_steps" form:"loop_detection_steps"`
CanStopItself bool `json:"can_stop_itself" form:"can_stop_itself"`
SystemPrompt string `json:"system_prompt" form:"system_prompt"`
LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"`
@@ -146,6 +147,27 @@ func NewAgentConfigMeta(
DefaultValue: "",
Tags: config.Tags{Section: "ModelSettings"},
},
{
Name: "transcription_model",
Label: "Transcription Model",
Type: "text",
DefaultValue: "",
Tags: config.Tags{Section: "ModelSettings"},
},
{
Name: "transcription_language",
Label: "Transcription Language",
Type: "text",
DefaultValue: "",
Tags: config.Tags{Section: "ModelSettings"},
},
{
Name: "tts_model",
Label: "TTS Model",
Type: "text",
DefaultValue: "",
Tags: config.Tags{Section: "ModelSettings"},
},
{
Name: "api_url",
Label: "API URL",
@@ -269,15 +291,6 @@ func NewAgentConfigMeta(
HelpText: "Enable agent to explain its reasoning process",
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
Name: "loop_detection_steps",
Label: "Max Loop Detection Steps",
Type: "number",
DefaultValue: 5,
Min: 1,
Step: 1,
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
Name: "parallel_jobs",
Label: "Parallel Jobs",
@@ -301,7 +314,7 @@ func NewAgentConfigMeta(
Label: "MCP Prepare Script",
Type: "textarea",
DefaultValue: "",
HelpText: "Script to prepare the MCP box",
HelpText: "Script to prepare for running MCP servers",
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
+81 -68
View File
@@ -26,21 +26,21 @@ import (
type AgentPool struct {
sync.Mutex
file string
pooldir string
pool AgentPoolData
agents map[string]*Agent
managers map[string]sse.Manager
agentStatus map[string]*Status
apiURL, defaultModel, defaultMultimodalModel string
mcpBoxURL string
imageModel, localRAGAPI, localRAGKey, apiKey string
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action
connectors func(*AgentConfig) []Connector
dynamicPrompt func(*AgentConfig) []DynamicPrompt
filters func(*AgentConfig) types.JobFilters
timeout string
conversationLogs string
file string
pooldir string
pool AgentPoolData
agents map[string]*Agent
managers map[string]sse.Manager
agentStatus map[string]*Status
apiURL, defaultModel, defaultMultimodalModel, defaultTTSModel string
defaultTranscriptionModel, defaultTranscriptionLanguage string
imageModel, localRAGAPI, localRAGKey, apiKey string
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action
connectors func(*AgentConfig) []Connector
dynamicPrompt func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []DynamicPrompt
filters func(*AgentConfig) types.JobFilters
timeout string
conversationLogs string
}
type Status struct {
@@ -74,11 +74,11 @@ func loadPoolFromFile(path string) (*AgentPoolData, error) {
}
func NewAgentPool(
defaultModel, defaultMultimodalModel, imageModel, apiURL, apiKey, directory, mcpBoxURL string,
defaultModel, defaultMultimodalModel, defaultTranscriptionModel, defaultTranscriptionLanguage, defaultTTSModel, imageModel, apiURL, apiKey, directory string,
LocalRAGAPI string,
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action,
connectors func(*AgentConfig) []Connector,
promptBlocks func(*AgentConfig) []DynamicPrompt,
promptBlocks func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []DynamicPrompt,
filters func(*AgentConfig) types.JobFilters,
timeout string,
withLogs bool,
@@ -96,25 +96,27 @@ func NewAgentPool(
if _, err := os.Stat(poolfile); err != nil {
// file does not exist, create a new pool
return &AgentPool{
file: poolfile,
pooldir: directory,
apiURL: apiURL,
defaultModel: defaultModel,
defaultMultimodalModel: defaultMultimodalModel,
mcpBoxURL: mcpBoxURL,
imageModel: imageModel,
localRAGAPI: LocalRAGAPI,
apiKey: apiKey,
agents: make(map[string]*Agent),
pool: make(map[string]AgentConfig),
agentStatus: make(map[string]*Status),
managers: make(map[string]sse.Manager),
connectors: connectors,
availableActions: availableActions,
dynamicPrompt: promptBlocks,
filters: filters,
timeout: timeout,
conversationLogs: conversationPath,
file: poolfile,
pooldir: directory,
apiURL: apiURL,
defaultModel: defaultModel,
defaultMultimodalModel: defaultMultimodalModel,
defaultTranscriptionModel: defaultTranscriptionModel,
defaultTranscriptionLanguage: defaultTranscriptionLanguage,
defaultTTSModel: defaultTTSModel,
imageModel: imageModel,
localRAGAPI: LocalRAGAPI,
apiKey: apiKey,
agents: make(map[string]*Agent),
pool: make(map[string]AgentConfig),
agentStatus: make(map[string]*Status),
managers: make(map[string]sse.Manager),
connectors: connectors,
availableActions: availableActions,
dynamicPrompt: promptBlocks,
filters: filters,
timeout: timeout,
conversationLogs: conversationPath,
}, nil
}
@@ -123,25 +125,27 @@ func NewAgentPool(
return nil, err
}
return &AgentPool{
file: poolfile,
apiURL: apiURL,
pooldir: directory,
defaultModel: defaultModel,
defaultMultimodalModel: defaultMultimodalModel,
mcpBoxURL: mcpBoxURL,
imageModel: imageModel,
apiKey: apiKey,
agents: make(map[string]*Agent),
managers: make(map[string]sse.Manager),
agentStatus: map[string]*Status{},
pool: *poolData,
connectors: connectors,
localRAGAPI: LocalRAGAPI,
dynamicPrompt: promptBlocks,
filters: filters,
availableActions: availableActions,
timeout: timeout,
conversationLogs: conversationPath,
file: poolfile,
apiURL: apiURL,
pooldir: directory,
defaultModel: defaultModel,
defaultMultimodalModel: defaultMultimodalModel,
defaultTranscriptionModel: defaultTranscriptionModel,
defaultTranscriptionLanguage: defaultTranscriptionLanguage,
defaultTTSModel: defaultTTSModel,
imageModel: imageModel,
apiKey: apiKey,
agents: make(map[string]*Agent),
managers: make(map[string]sse.Manager),
agentStatus: map[string]*Status{},
pool: *poolData,
connectors: connectors,
localRAGAPI: LocalRAGAPI,
dynamicPrompt: promptBlocks,
filters: filters,
availableActions: availableActions,
timeout: timeout,
conversationLogs: conversationPath,
}, nil
}
@@ -334,21 +338,31 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
ctx := context.Background()
model := a.defaultModel
multimodalModel := a.defaultMultimodalModel
transcriptionModel := a.defaultTranscriptionModel
transcriptionLanguage := a.defaultTranscriptionLanguage
ttsModel := a.defaultTTSModel
if config.MultimodalModel != "" {
multimodalModel = config.MultimodalModel
}
if config.TranscriptionModel != "" {
transcriptionModel = config.TranscriptionModel
}
if config.TranscriptionLanguage != "" {
transcriptionLanguage = config.TranscriptionLanguage
}
if config.TTSModel != "" {
ttsModel = config.TTSModel
}
if config.Model != "" {
model = config.Model
} else {
config.Model = model
}
if config.MCPBoxURL != "" {
a.mcpBoxURL = config.MCPBoxURL
}
if config.PeriodicRuns == "" {
config.PeriodicRuns = "10m"
}
@@ -375,7 +389,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
}
connectors := a.connectors(config)
promptBlocks := a.dynamicPrompt(config)
promptBlocks := a.dynamicPrompt(config)(ctx, a)
actions := a.availableActions(config)(ctx, a)
filters := a.filters(config)
stateFile, characterFile := a.stateFiles(name)
@@ -419,10 +433,12 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
WithLLMAPIURL(a.apiURL),
WithContext(ctx),
WithMCPServers(config.MCPServers...),
WithTranscriptionModel(transcriptionModel),
WithTranscriptionLanguage(transcriptionLanguage),
WithTTSModel(ttsModel),
WithPeriodicRuns(config.PeriodicRuns),
WithPermanentGoal(config.PermanentGoal),
WithMCPSTDIOServers(config.MCPSTDIOServers...),
WithMCPBoxURL(a.mcpBoxURL),
WithPrompts(promptBlocks...),
WithJobFilters(filters...),
WithMCPPrepareScript(config.MCPPrepareScript),
@@ -554,19 +570,16 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults))
}
if config.LoopDetectionSteps > 0 {
opts = append(opts, WithLoopDetectionSteps(config.LoopDetectionSteps))
}
if config.ParallelJobs > 0 {
opts = append(opts, WithParallelJobs(config.ParallelJobs))
}
if config.EnableEvaluation {
opts = append(opts, EnableEvaluation())
if config.MaxEvaluationLoops > 0 {
opts = append(opts, WithMaxEvaluationLoops(config.MaxEvaluationLoops))
}
}
if config.MaxEvaluationLoops > 0 {
opts = append(opts, WithMaxEvaluationLoops(config.MaxEvaluationLoops))
}
xlog.Info("Starting agent", "name", name, "config", config)
+41 -7
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"github.com/mudler/cogito"
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
)
@@ -30,9 +31,10 @@ func NewActionContext(ctx context.Context, cancel context.CancelFunc) *ActionCon
type ActionParams map[string]interface{}
type ActionResult struct {
Job *Job
Result string
Metadata map[string]interface{}
Job *Job
Result string
ImageBase64Result string
Metadata map[string]interface{}
}
func (ap ActionParams) Read(s string) error {
@@ -87,11 +89,35 @@ func (a ActionDefinition) ToFunctionDefinition() *openai.FunctionDefinition {
}
}
type cogitoWrapper struct {
action Action
ctx context.Context
sharedState *AgentSharedState
}
func (c *cogitoWrapper) Tool() openai.Tool {
return openai.Tool{
Type: openai.ToolTypeFunction,
Function: c.action.Definition().ToFunctionDefinition(),
}
}
func (c *cogitoWrapper) Run(args map[string]any) (string, error) {
ctx := c.ctx
if ctx == nil {
ctx = context.Background()
}
result, err := c.action.Run(ctx, c.sharedState, ActionParams(args))
if err != nil {
return "", err
}
return result.Result, nil
}
// Actions is something the agent can do
type Action interface {
Run(ctx context.Context, sharedState *AgentSharedState, action ActionParams) (ActionResult, error)
Definition() ActionDefinition
Plannable() bool
}
// UserDefinedChecker interface to identify user-defined actions
@@ -141,9 +167,9 @@ func (u *UserDefinedAction) IsUserDefined() bool {
func CreateUserDefinedActions(userTools []ActionDefinition) []Action {
var actions []Action
for _, tool := range userTools {
actions = append(actions, &UserDefinedAction{
ActionDef: &tool,
})
actions = append(actions, &UserDefinedAction{
ActionDef: &tool,
})
}
return actions
}
@@ -161,6 +187,14 @@ func (a Actions) ToTools() []openai.Tool {
return tools
}
func (a Actions) ToCogitoTools(ctx context.Context, sharedState *AgentSharedState) []cogito.Tool {
tools := []cogito.Tool{}
for _, action := range a {
tools = append(tools, &cogitoWrapper{action: action, ctx: ctx, sharedState: sharedState})
}
return tools
}
func (a Actions) Find(name string) Action {
for _, action := range a {
if action.Definition().Name.Is(name) {
+5 -39
View File
@@ -5,6 +5,7 @@ import (
"log"
"github.com/google/uuid"
"github.com/mudler/cogito"
"github.com/sashabaranov/go-openai"
)
@@ -20,19 +21,15 @@ type Job struct {
UUID string
Metadata map[string]interface{}
DoneFilter bool
// Tools available for this job
BuiltinTools []ActionDefinition // Built-in tools like web search
UserTools []ActionDefinition // User-defined function tools
ToolChoice string
pastActions []*ActionRequest
nextAction *Action
nextActionParams *ActionParams
nextActionReasoning string
context context.Context
cancel context.CancelFunc
context context.Context
fragment *cogito.Fragment
cancel context.CancelFunc
Obs *Observable
}
@@ -108,37 +105,6 @@ func (j *Job) CallbackWithResult(stateResult ActionState) {
j.ResultCallback(stateResult)
}
func (j *Job) SetNextAction(action *Action, params *ActionParams, reasoning string) {
j.nextAction = action
j.nextActionParams = params
j.nextActionReasoning = reasoning
}
func (j *Job) AddPastAction(action Action, params *ActionParams) {
j.pastActions = append(j.pastActions, &ActionRequest{
Action: action,
Params: params,
})
}
func (j *Job) GetPastActions() []*ActionRequest {
return j.pastActions
}
func (j *Job) GetNextAction() (*Action, *ActionParams, string) {
return j.nextAction, j.nextActionParams, j.nextActionReasoning
}
func (j *Job) HasNextAction() bool {
return j.nextAction != nil
}
func (j *Job) ResetNextAction() {
j.nextAction = nil
j.nextActionParams = nil
j.nextActionReasoning = ""
}
func WithTextImage(text, image string) JobOption {
return func(j *Job) {
j.ConversationHistory = append(j.ConversationHistory, openai.ChatCompletionMessage{
+6
View File
@@ -0,0 +1,6 @@
package types
type PromptResult struct {
Content string
ImageBase64 string
}
+2
View File
@@ -3,6 +3,7 @@ package types
import (
"sync"
"github.com/mudler/cogito"
"github.com/sashabaranov/go-openai"
)
@@ -11,6 +12,7 @@ type JobResult struct {
sync.Mutex
// The result of a job
State []ActionState
Plans []cogito.PlanStatus
Conversation []openai.ChatCompletionMessage
Finalizers []func([]openai.ChatCompletionMessage)
-5
View File
@@ -11,11 +11,6 @@ services:
- /dev/dri
- /dev/kfd
mcpbox:
extends:
file: docker-compose.yaml
service: mcpbox
dind:
extends:
file: docker-compose.yaml
+1 -6
View File
@@ -6,17 +6,12 @@ services:
environment:
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
- DEBUG=true
image: localai/localai:master-gpu-intel-f32
image: localai/localai:master-gpu-intel
devices:
# On a system with integrated GPU and an Arc 770, this is the Arc 770
- /dev/dri/card1
- /dev/dri/renderD129
mcpbox:
extends:
file: docker-compose.yaml
service: mcpbox
dind:
extends:
file: docker-compose.yaml
+1 -6
View File
@@ -6,7 +6,7 @@ services:
environment:
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
- DEBUG=true
image: localai/localai:master-gpu-nvidia-cuda12
image: localai/localai:master-gpu-nvidia-cuda-12
# For images with python backends, use:
# image: localai/localai:master-cublas-cuda12-ffmpeg
deploy:
@@ -17,11 +17,6 @@ services:
count: 1
capabilities: [gpu]
mcpbox:
extends:
file: docker-compose.yaml
service: mcpbox
dind:
extends:
file: docker-compose.yaml
+2 -21
View File
@@ -61,25 +61,6 @@ services:
dind:
condition: service_healthy
mcpbox:
build:
context: .
dockerfile: Dockerfile.mcpbox
ports:
- "8080"
volumes:
- ./volumes/mcpbox:/app/data
environment:
- DOCKER_HOST=tcp://dind:2375
depends_on:
dind:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/processes"]
interval: 30s
timeout: 10s
retries: 3
dind:
image: docker:dind
privileged: true
@@ -97,7 +78,7 @@ services:
condition: service_healthy
localrecall-healthcheck:
condition: service_completed_successfully
mcpbox:
dind:
condition: service_healthy
build:
context: .
@@ -115,8 +96,8 @@ services:
- LOCALAGI_STATE_DIR=/pool
- LOCALAGI_TIMEOUT=5m
- LOCALAGI_ENABLE_CONVERSATIONS_LOGGING=false
- LOCALAGI_MCPBOX_URL=http://mcpbox:8080
- LOCALAGI_SSHBOX_URL=root:root@sshbox:22
- DOCKER_HOST=tcp://dind:2375
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
+56
View File
@@ -0,0 +1,56 @@
package custom_actions
import (
"encoding/json"
)
type Params struct {
Message string `json:"message"` // field name
}
func Run(config map[string]interface{}) (string, map[string]interface{}, error) {
p := Params{}
b, err := json.Marshal(config)
if err != nil {
return "", map[string]interface{}{}, err
}
if err := json.Unmarshal(b, &p); err != nil {
return "", map[string]interface{}{}, err
}
return "Hello, " + p.Message + "!", map[string]interface{}{}, nil
}
func Description() string {
return "Send a message to the user"
}
func Definition() map[string][]string {
return map[string][]string{
"message": { // field name
"string", // type
"The message to send", // description
},
}
}
func RequiredFields() []string {
return []string{"message"} // field name
}
var config string
func Init(configuration string) error {
// Do something with the configuration that was passed-by
config = configuration
return nil
}
// DynamicPrompt
func Render() (string, string, error) {
return "Hello, " + config + "!", "", nil
}
func Role() string {
return "system" // Role for the dynamic prompt
}
-12
View File
@@ -1,12 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from RealtimeSTT import AudioToTextRecorder
def process_text(text):
print(text)
if __name__ == '__main__':
recorder = AudioToTextRecorder(wake_words="jarvis")
while True:
recorder.text(process_text)
+39 -55
View File
@@ -1,92 +1,81 @@
module github.com/mudler/LocalAGI
go 1.24
toolchain go1.24.2
go 1.24.4
require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/bwmarrin/discordgo v0.29.0
github.com/chasefleming/elem-go v0.30.0
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2
github.com/donseba/go-htmx v1.12.0
github.com/eritikass/githubmarkdownconvertergo v0.1.10
github.com/go-telegram/bot v1.15.0
github.com/gofiber/fiber/v2 v2.52.8
github.com/gofiber/template/html/v2 v2.1.3
github.com/go-telegram/bot v1.17.0
github.com/gofiber/fiber/v2 v2.52.9
github.com/google/go-github/v69 v69.2.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/metoro-io/mcp-golang v0.13.0
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0
github.com/modelcontextprotocol/go-sdk v1.1.0
github.com/mudler/cogito v0.5.1
github.com/onsi/ginkgo/v2 v2.25.3
github.com/onsi/gomega v1.38.2
github.com/philippgille/chromem-go v0.7.0
github.com/robfig/cron/v3 v3.0.1
github.com/sashabaranov/go-openai v1.40.3
github.com/sashabaranov/go-openai v1.41.2
github.com/slack-go/slack v0.17.3
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
github.com/tmc/langchaingo v0.1.13
github.com/tmc/langchaingo v0.1.14
github.com/traefik/yaegi v0.16.1
github.com/valyala/fasthttp v1.62.0
golang.org/x/crypto v0.40.0
github.com/valyala/fasthttp v1.68.0
golang.org/x/crypto v0.43.0
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
maunium.net/go/mautrix v0.17.0
mvdan.cc/xurls/v2 v2.6.0
)
require github.com/JohannesKaufmann/dom v0.2.0 // indirect
require (
dario.cat/mergo v1.0.2 // indirect
github.com/JohannesKaufmann/dom v0.2.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/google/jsonschema-go v0.3.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
)
require (
github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.3
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/antchfx/htmlquery v1.3.4 // indirect
github.com/antchfx/xmlquery v1.4.4 // indirect
github.com/antchfx/xpath v1.3.4 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emersion/go-imap/v2 v2.0.0-beta.5
github.com/emersion/go-message v0.18.2
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
github.com/emersion/go-smtp v0.22.0
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/emersion/go-smtp v0.24.0
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gocolly/colly v1.2.0 // indirect
github.com/gofiber/template v1.8.3 // indirect
github.com/gofiber/utils v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kennygrant/sanitize v1.2.4 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkoukk/tiktoken-go v0.1.7 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.31.0 // indirect
@@ -97,21 +86,16 @@ require (
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
go.mau.fi/util v0.3.0 // indirect
go.starlark.net v0.0.0-20250417143717-f57e51f710eb // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
golang.org/x/arch v0.16.0 // indirect
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa
golang.org/x/net v0.46.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
google.golang.org/protobuf v1.36.8 // indirect
maunium.net/go/maulogger/v2 v2.4.1 // indirect
)
+169 -125
View File
@@ -1,11 +1,23 @@
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ=
github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.3 h1:r3fokGFRDk/8pHmwLwJ8zsX4qiqfS1/1TZm2BH8ueY8=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.3/go.mod h1:HtsP+1Fchp4dVvaiIsLHAl/yqL3H1YLwqLC9kNwqQEg=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0 h1:C0/TerKdQX9Y9pbYi1EsLr5LDNANsqunyI/btpyfCg8=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0/go.mod h1:OLaKh+giepO8j7teevrNwiy/fwf8LXgoc9g7rwaE1jk=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/antchfx/htmlquery v1.3.4 h1:Isd0srPkni2iNTWCwVj/72t7uCphFeor5Q8nCzj1jdQ=
@@ -15,79 +27,76 @@ github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fus
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4=
github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/chasefleming/elem-go v0.30.0 h1:BlhV1ekv1RbFiM8XZUQeln1Ikb4D+bu2eDO4agREvok=
github.com/chasefleming/elem-go v0.30.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2 h1:flLYmnQFZNo04x2NPehMbf30m7Pli57xwZ0NFqR/hb0=
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2/go.mod h1:NtWqRzAp/1tw+twkW8uuBenEVVYndEAZACWU3F3xdoQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/donseba/go-htmx v1.12.0 h1:7tESER0uxaqsuGMv3yP3pK1drfBUXM6apG4H7/3+IgE=
github.com/donseba/go-htmx v1.12.0/go.mod h1:8PTAYvNKf8+QYis+DpAsggKz+sa2qljtMgvdAeNBh5s=
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/emersion/go-imap/v2 v2.0.0-beta.5 h1:H3858DNmBuXyMK1++YrQIRdpKE1MwBc+ywBtg3n+0wA=
github.com/emersion/go-imap/v2 v2.0.0-beta.5/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk=
github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.22.0 h1:/d3HWxkZZ4riB+0kzfoODh9X+xyCrLEezMnAAa1LEMU=
github.com/emersion/go-smtp v0.22.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
github.com/eritikass/githubmarkdownconvertergo v0.1.10 h1:mL93ADvYMOeT15DcGtK9AaFFc+RcWcy6kQBC6yS/5f4=
github.com/eritikass/githubmarkdownconvertergo v0.1.10/go.mod h1:BdpHs6imOtzE5KorbUtKa6bZ0ZBh1yFcrTTAL8FwDKY=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-telegram/bot v1.15.0 h1:/ba5pp084MUhjR5sQDymQ7JNZ001CQa7QjtxLWcuGpg=
github.com/go-telegram/bot v1.15.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
github.com/go-telegram/bot v1.17.0 h1:Hs0kGxSj97QFqOQP0zxduY/4tSx8QDzvNI9uVRS+zmY=
github.com/go-telegram/bot v1.17.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4=
github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
@@ -106,7 +115,8 @@ github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzea
github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -114,26 +124,20 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -144,29 +148,51 @@ 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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/metoro-io/mcp-golang v0.13.0 h1:54TFBJIW76VRB55CJovQQje9x4GnXg0BQQwGRtXrbCE=
github.com/metoro-io/mcp-golang v0.13.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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA=
github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mudler/cogito v0.5.1 h1:KK9F7pNJUopewiZRsKnDEGu49+22fPrxWYxpoWQWN7s=
github.com/mudler/cogito v0.5.1/go.mod h1:2uhEElCTq8eXSsqJ1JF01oA5h9niXSELVKqCF1PqjEw=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw=
github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY=
github.com/philippgille/chromem-go v0.7.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -181,28 +207,34 @@ github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sashabaranov/go-openai v1.40.3 h1:PkOw0SK34wrvYVOuXF1HZzuTBRh992qRZHil4kG3eYE=
github.com/sashabaranov/go-openai v1.40.3/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY=
github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=
github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w=
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 h1:l/T7dYuJEQZOwVOpjIXr1180aM9PZL/d1MnMVIxefX4=
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64/go.mod h1:Q1NAJOuRdQCqN/VIWdnaaEhV8LpeO2rtlBP7/iDJNII=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -215,33 +247,45 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA=
github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/tmc/langchaingo v0.1.14 h1:o1qWBPigAIuFvrG6cjTFo0cZPFEZ47ZqpOYMjM15yZc=
github.com/tmc/langchaingo v0.1.14/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378=
github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0=
github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo=
github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mau.fi/util v0.3.0 h1:Lt3lbRXP6ZBqTINK0EieRWor3zEwwwrDT14Z5N8RUCs=
go.mau.fi/util v0.3.0/go.mod h1:9dGsBCCbZJstx16YgnVMVi3O2bOizELoKpugLD4FoGs=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.starlark.net v0.0.0-20250417143717-f57e51f710eb h1:zOg9DxxrorEmgGUr5UPdCEwKqiqG0MlZciuCuA3XiDE=
go.starlark.net v0.0.0-20250417143717-f57e51f710eb/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -249,10 +293,10 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -268,8 +312,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -291,8 +337,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -302,8 +348,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -315,30 +361,29 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g=
@@ -349,6 +394,5 @@ maunium.net/go/mautrix v0.17.0 h1:scc1qlUbzPn+wc+3eAPquyD+3gZwwy/hBANBm+iGKK8=
maunium.net/go/mautrix v0.17.0/go.mod h1:j+puTEQCEydlVxhJ/dQP5chfa26TdvBO7X6F3Ataav8=
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
+14 -3
View File
@@ -13,6 +13,9 @@ import (
var baseModel = os.Getenv("LOCALAGI_MODEL")
var multimodalModel = os.Getenv("LOCALAGI_MULTIMODAL_MODEL")
var transcriptionModel = os.Getenv("LOCALAGI_TRANSCRIPTION_MODEL")
var transcriptionLanguage = os.Getenv("LOCALAGI_TRANSCRIPTION_LANGUAGE")
var ttsModel = os.Getenv("LOCALAGI_TTS_MODEL")
var apiURL = os.Getenv("LOCALAGI_LLM_API_URL")
var apiKey = os.Getenv("LOCALAGI_LLM_API_KEY")
var timeout = os.Getenv("LOCALAGI_TIMEOUT")
@@ -23,7 +26,7 @@ var apiKeysEnv = os.Getenv("LOCALAGI_API_KEYS")
var imageModel = os.Getenv("LOCALAGI_IMAGE_MODEL")
var conversationDuration = os.Getenv("LOCALAGI_CONVERSATION_DURATION")
var localOperatorBaseURL = os.Getenv("LOCALOPERATOR_BASE_URL")
var mcpboxURL = os.Getenv("LOCALAGI_MCPBOX_URL")
var customActionsDir = os.Getenv("LOCALAGI_CUSTOM_ACTIONS_DIR")
var sshBoxURL = os.Getenv("LOCALAGI_SSHBOX_URL")
func init() {
@@ -59,19 +62,26 @@ func main() {
pool, err := state.NewAgentPool(
baseModel,
multimodalModel,
transcriptionModel,
transcriptionLanguage,
ttsModel,
imageModel,
apiURL,
apiKey,
stateDir,
mcpboxURL,
localRAG,
services.Actions(map[string]string{
services.ActionConfigBrowserAgentRunner: localOperatorBaseURL,
services.ActionConfigDeepResearchRunner: localOperatorBaseURL,
services.ActionConfigSSHBoxURL: sshBoxURL,
services.ConfigStateDir: stateDir,
services.CustomActionsDir: customActionsDir,
}),
services.Connectors,
services.DynamicPrompts,
services.DynamicPrompts(map[string]string{
services.ConfigStateDir: stateDir,
services.CustomActionsDir: customActionsDir,
}),
services.Filters,
timeout,
withLogs,
@@ -88,6 +98,7 @@ func main() {
webui.WithLLMAPIUrl(apiURL),
webui.WithLLMAPIKey(apiKey),
webui.WithLLMModel(baseModel),
webui.WithCustomActionsDir(customActionsDir),
webui.WithStateDir(stateDir),
)
+7 -7
View File
@@ -8,13 +8,13 @@ import (
// AgentConfig represents the configuration for an agent
type AgentConfig struct {
Name string `json:"name"`
Actions []string `json:"actions,omitempty"`
Connectors []string `json:"connectors,omitempty"`
PromptBlocks []string `json:"prompt_blocks,omitempty"`
InitialPrompt string `json:"initial_prompt,omitempty"`
Parallel bool `json:"parallel,omitempty"`
Config map[string]interface{} `json:"config,omitempty"`
Name string `json:"name"`
Actions []string `json:"actions,omitempty"`
Connectors []string `json:"connectors,omitempty"`
PromptBlocks []string `json:"prompt_blocks,omitempty"`
InitialPrompt string `json:"initial_prompt,omitempty"`
Parallel bool `json:"parallel,omitempty"`
EnableReasoning bool `json:"enable_reasoning,omitempty"`
}
// AgentStatus represents the status of an agent
-325
View File
@@ -1,325 +0,0 @@
package stdio
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"sync"
"time"
"github.com/gorilla/websocket"
)
// Client implements the transport.Interface for stdio processes
type Client struct {
baseURL string
processes map[string]*Process
groups map[string][]string
mu sync.RWMutex
}
// NewClient creates a new stdio transport client
func NewClient(baseURL string) *Client {
return &Client{
baseURL: baseURL,
processes: make(map[string]*Process),
groups: make(map[string][]string),
}
}
// CreateProcess starts a new process in a group
func (c *Client) CreateProcess(ctx context.Context, command string, args []string, env []string, groupID string) (*Process, error) {
log.Printf("Creating process: command=%s, args=%v, groupID=%s", command, args, groupID)
req := struct {
Command string `json:"command"`
Args []string `json:"args"`
Env []string `json:"env"`
GroupID string `json:"group_id"`
}{
Command: command,
Args: args,
Env: env,
GroupID: groupID,
}
reqBody, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
url := fmt.Sprintf("%s/processes", c.baseURL)
log.Printf("Sending POST request to %s", url)
resp, err := http.Post(url, "application/json", bytes.NewReader(reqBody))
if err != nil {
return nil, fmt.Errorf("failed to start process: %w", err)
}
defer resp.Body.Close()
log.Printf("Received response with status: %d", resp.StatusCode)
var result struct {
ID string `json:"id"`
}
body, _ := io.ReadAll(resp.Body)
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w. body: %s", err, string(body))
}
log.Printf("Successfully created process with ID: %s", result.ID)
process := &Process{
ID: result.ID,
GroupID: groupID,
CreatedAt: time.Now(),
}
c.mu.Lock()
c.processes[process.ID] = process
if groupID != "" {
c.groups[groupID] = append(c.groups[groupID], process.ID)
}
c.mu.Unlock()
return process, nil
}
// GetProcess returns a process by ID
func (c *Client) GetProcess(id string) (*Process, error) {
c.mu.RLock()
process, exists := c.processes[id]
c.mu.RUnlock()
if !exists {
return nil, fmt.Errorf("process not found: %s", id)
}
return process, nil
}
// GetGroupProcesses returns all processes in a group
func (c *Client) GetGroupProcesses(groupID string) ([]*Process, error) {
c.mu.RLock()
processIDs, exists := c.groups[groupID]
if !exists {
c.mu.RUnlock()
return nil, fmt.Errorf("group not found: %s", groupID)
}
processes := make([]*Process, 0, len(processIDs))
for _, pid := range processIDs {
if process, exists := c.processes[pid]; exists {
processes = append(processes, process)
}
}
c.mu.RUnlock()
return processes, nil
}
// StopProcess stops a single process
func (c *Client) StopProcess(id string) error {
c.mu.Lock()
process, exists := c.processes[id]
if !exists {
c.mu.Unlock()
return fmt.Errorf("process not found: %s", id)
}
// Remove from group if it exists
if process.GroupID != "" {
groupProcesses := c.groups[process.GroupID]
for i, pid := range groupProcesses {
if pid == id {
c.groups[process.GroupID] = append(groupProcesses[:i], groupProcesses[i+1:]...)
break
}
}
if len(c.groups[process.GroupID]) == 0 {
delete(c.groups, process.GroupID)
}
}
delete(c.processes, id)
c.mu.Unlock()
req, err := http.NewRequest(
"DELETE",
fmt.Sprintf("%s/processes/%s", c.baseURL, id),
nil,
)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed to stop process: %w", err)
}
resp.Body.Close()
return nil
}
// StopGroup stops all processes in a group
func (c *Client) StopGroup(groupID string) error {
c.mu.Lock()
processIDs, exists := c.groups[groupID]
if !exists {
c.mu.Unlock()
return fmt.Errorf("group not found: %s", groupID)
}
c.mu.Unlock()
for _, pid := range processIDs {
if err := c.StopProcess(pid); err != nil {
return fmt.Errorf("failed to stop process %s in group %s: %w", pid, groupID, err)
}
}
return nil
}
// ListGroups returns all group IDs
func (c *Client) ListGroups() []string {
c.mu.RLock()
defer c.mu.RUnlock()
groups := make([]string, 0, len(c.groups))
for groupID := range c.groups {
groups = append(groups, groupID)
}
return groups
}
// GetProcessIO returns io.Reader and io.Writer for a process
func (c *Client) GetProcessIO(id string) (io.Reader, io.Writer, error) {
log.Printf("Getting IO for process: %s", id)
process, err := c.GetProcess(id)
if err != nil {
return nil, nil, err
}
// Parse the base URL to get the host
baseURL, err := url.Parse(c.baseURL)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse base URL: %w", err)
}
// Connect to WebSocket
u := url.URL{
Scheme: "ws",
Host: baseURL.Host,
Path: fmt.Sprintf("/ws/%s", process.ID),
}
log.Printf("Connecting to WebSocket at: %s", u.String())
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
return nil, nil, fmt.Errorf("failed to connect to WebSocket: %w", err)
}
log.Printf("Successfully connected to WebSocket for process: %s", id)
// Create reader and writer
reader := &websocketReader{conn: conn}
writer := &websocketWriter{conn: conn}
return reader, writer, nil
}
// websocketReader implements io.Reader for WebSocket
type websocketReader struct {
conn *websocket.Conn
}
func (r *websocketReader) Read(p []byte) (n int, err error) {
_, message, err := r.conn.ReadMessage()
if err != nil {
return 0, err
}
n = copy(p, message)
return n, nil
}
// websocketWriter implements io.Writer for WebSocket
type websocketWriter struct {
conn *websocket.Conn
}
func (w *websocketWriter) Write(p []byte) (n int, err error) {
// Use BinaryMessage type for better compatibility
err = w.conn.WriteMessage(websocket.BinaryMessage, p)
if err != nil {
return 0, fmt.Errorf("failed to write WebSocket message: %w", err)
}
return len(p), nil
}
// Close closes all connections and stops all processes
func (c *Client) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
// Stop all processes
for id := range c.processes {
if err := c.StopProcess(id); err != nil {
return fmt.Errorf("failed to stop process %s: %w", id, err)
}
}
return nil
}
// RunProcess executes a command and returns its output
func (c *Client) RunProcess(ctx context.Context, command string, args []string, env []string) (string, error) {
log.Printf("Running one-time process: command=%s, args=%v", command, args)
req := struct {
Command string `json:"command"`
Args []string `json:"args"`
Env []string `json:"env"`
}{
Command: command,
Args: args,
Env: env,
}
reqBody, err := json.Marshal(req)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}
url := fmt.Sprintf("%s/run", c.baseURL)
log.Printf("Sending POST request to %s", url)
resp, err := http.Post(url, "application/json", bytes.NewReader(reqBody))
if err != nil {
return "", fmt.Errorf("failed to execute process: %w", err)
}
defer resp.Body.Close()
log.Printf("Received response with status: %d", resp.StatusCode)
var result struct {
Output string `json:"output"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("failed to decode response: %w. body: %s", err, string(body))
}
log.Printf("Successfully executed process with output length: %d", len(result.Output))
return result.Output, nil
}
-28
View File
@@ -1,28 +0,0 @@
package stdio
import (
"os"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestSTDIOTransport(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "STDIOTransport test suite")
}
var baseURL string
func init() {
baseURL = os.Getenv("LOCALAGI_MCPBOX_URL")
if baseURL == "" {
baseURL = "http://localhost:8080"
}
}
var _ = AfterSuite(func() {
client := NewClient(baseURL)
client.StopGroup("test-group")
})
-235
View File
@@ -1,235 +0,0 @@
package stdio
import (
"context"
"time"
mcp "github.com/metoro-io/mcp-golang"
"github.com/metoro-io/mcp-golang/transport/stdio"
"github.com/mudler/LocalAGI/pkg/xlog"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Client", func() {
var (
client *Client
)
BeforeEach(func() {
client = NewClient(baseURL)
})
AfterEach(func() {
if client != nil {
Expect(client.Close()).To(Succeed())
}
})
Context("Process Management", func() {
It("should create and stop a process", func() {
ctx := context.Background()
// Use a command that doesn't exit immediately
process, err := client.CreateProcess(ctx, "sh", []string{"-c", "echo 'Hello, World!'; sleep 10"}, []string{}, "test-group")
Expect(err).NotTo(HaveOccurred())
Expect(process).NotTo(BeNil())
Expect(process.ID).NotTo(BeEmpty())
// Get process IO
reader, writer, err := client.GetProcessIO(process.ID)
Expect(err).NotTo(HaveOccurred())
Expect(reader).NotTo(BeNil())
Expect(writer).NotTo(BeNil())
// Write to process
_, err = writer.Write([]byte("test input\n"))
Expect(err).NotTo(HaveOccurred())
// Read from process with timeout
buf := make([]byte, 1024)
readDone := make(chan struct{})
var readErr error
var readN int
go func() {
readN, readErr = reader.Read(buf)
close(readDone)
}()
// Wait for read with timeout
select {
case <-readDone:
Expect(readErr).NotTo(HaveOccurred())
Expect(readN).To(BeNumerically(">", 0))
Expect(string(buf[:readN])).To(ContainSubstring("Hello, World!"))
case <-time.After(5 * time.Second):
Fail("Timeout waiting for process output")
}
// Stop the process
err = client.StopProcess(process.ID)
Expect(err).NotTo(HaveOccurred())
})
It("should manage process groups", func() {
ctx := context.Background()
groupID := "test-group"
// Create multiple processes in the same group
process1, err := client.CreateProcess(ctx, "sh", []string{"-c", "echo 'Process 1'; sleep 1"}, []string{}, groupID)
Expect(err).NotTo(HaveOccurred())
Expect(process1).NotTo(BeNil())
process2, err := client.CreateProcess(ctx, "sh", []string{"-c", "echo 'Process 2'; sleep 1"}, []string{}, groupID)
Expect(err).NotTo(HaveOccurred())
Expect(process2).NotTo(BeNil())
// Get group processes
processes, err := client.GetGroupProcesses(groupID)
Expect(err).NotTo(HaveOccurred())
Expect(processes).To(HaveLen(2))
// List groups
groups := client.ListGroups()
Expect(groups).To(ContainElement(groupID))
// Stop the group
err = client.StopGroup(groupID)
Expect(err).NotTo(HaveOccurred())
})
It("should run a one-time process", func() {
ctx := context.Background()
output, err := client.RunProcess(ctx, "echo", []string{"One-time process"}, []string{})
Expect(err).NotTo(HaveOccurred())
Expect(output).To(ContainSubstring("One-time process"))
})
It("should handle process with environment variables", func() {
ctx := context.Background()
env := []string{"TEST_VAR=test_value"}
process, err := client.CreateProcess(ctx, "sh", []string{"-c", "env | grep TEST_VAR; sleep 1"}, env, "test-group")
Expect(err).NotTo(HaveOccurred())
Expect(process).NotTo(BeNil())
// Get process IO
reader, _, err := client.GetProcessIO(process.ID)
Expect(err).NotTo(HaveOccurred())
// Read environment variables with timeout
buf := make([]byte, 1024)
readDone := make(chan struct{})
var readErr error
var readN int
go func() {
readN, readErr = reader.Read(buf)
close(readDone)
}()
// Wait for read with timeout
select {
case <-readDone:
Expect(readErr).NotTo(HaveOccurred())
Expect(readN).To(BeNumerically(">", 0))
Expect(string(buf[:readN])).To(ContainSubstring("TEST_VAR=test_value"))
case <-time.After(5 * time.Second):
Fail("Timeout waiting for process output")
}
// Stop the process
err = client.StopProcess(process.ID)
Expect(err).NotTo(HaveOccurred())
})
It("should handle long-running processes", func() {
ctx := context.Background()
process, err := client.CreateProcess(ctx, "sh", []string{"-c", "echo 'Starting long process'; sleep 5"}, []string{}, "test-group")
Expect(err).NotTo(HaveOccurred())
Expect(process).NotTo(BeNil())
// Get process IO
reader, _, err := client.GetProcessIO(process.ID)
Expect(err).NotTo(HaveOccurred())
// Read initial output
buf := make([]byte, 1024)
readDone := make(chan struct{})
var readErr error
var readN int
go func() {
readN, readErr = reader.Read(buf)
close(readDone)
}()
// Wait for read with timeout
select {
case <-readDone:
Expect(readErr).NotTo(HaveOccurred())
Expect(readN).To(BeNumerically(">", 0))
Expect(string(buf[:readN])).To(ContainSubstring("Starting long process"))
case <-time.After(5 * time.Second):
Fail("Timeout waiting for process output")
}
// Wait a bit to ensure process is running
time.Sleep(time.Second)
// Stop the process
err = client.StopProcess(process.ID)
Expect(err).NotTo(HaveOccurred())
})
It("MCP", func() {
ctx := context.Background()
process, err := client.CreateProcess(ctx,
"docker", []string{"run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"},
[]string{"GITHUB_PERSONAL_ACCESS_TOKEN=test"}, "test-group")
Expect(err).NotTo(HaveOccurred())
Expect(process).NotTo(BeNil())
Expect(process.ID).NotTo(BeEmpty())
defer client.StopProcess(process.ID)
// MCP client
read, writer, err := client.GetProcessIO(process.ID)
Expect(err).NotTo(HaveOccurred())
Expect(read).NotTo(BeNil())
Expect(writer).NotTo(BeNil())
transport := stdio.NewStdioServerTransportWithIO(read, writer)
// Create a new client
mcpClient := mcp.NewClient(transport)
// Initialize the client
response, e := mcpClient.Initialize(ctx)
Expect(e).NotTo(HaveOccurred())
Expect(response).NotTo(BeNil())
Expect(mcpClient.Ping(ctx)).To(Succeed())
xlog.Debug("Client initialized: %v", response.Instructions)
alltools := []mcp.ToolRetType{}
var cursor *string
for {
tools, err := mcpClient.ListTools(ctx, cursor)
Expect(err).NotTo(HaveOccurred())
Expect(tools).NotTo(BeNil())
Expect(tools.Tools).NotTo(BeEmpty())
alltools = append(alltools, tools.Tools...)
if tools.NextCursor == nil {
break // No more pages
}
cursor = tools.NextCursor
}
for _, tool := range alltools {
xlog.Debug("Tool: %v", tool)
}
})
})
})
-473
View File
@@ -1,473 +0,0 @@
package stdio
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/mudler/LocalAGI/pkg/xlog"
)
// Process represents a running process with its stdio streams
type Process struct {
ID string
GroupID string
Cmd *exec.Cmd
Stdin io.WriteCloser
Stdout io.ReadCloser
Stderr io.ReadCloser
CreatedAt time.Time
}
// Server handles process management and stdio streaming
type Server struct {
processes map[string]*Process
groups map[string][]string // maps group ID to process IDs
mu sync.RWMutex
upgrader websocket.Upgrader
}
// NewServer creates a new stdio server
func NewServer() *Server {
return &Server{
processes: make(map[string]*Process),
groups: make(map[string][]string),
upgrader: websocket.Upgrader{},
}
}
// StartProcess starts a new process and returns its ID
func (s *Server) StartProcess(ctx context.Context, command string, args []string, env []string, groupID string) (string, error) {
xlog.Debug("Starting process", "command", command, "args", args, "groupID", groupID)
cmd := exec.CommandContext(ctx, command, args...)
if len(env) > 0 {
cmd.Env = append(os.Environ(), env...)
xlog.Debug("Process environment", "env", cmd.Env)
}
stdin, err := cmd.StdinPipe()
if err != nil {
return "", fmt.Errorf("failed to create stdin pipe: %w", err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return "", fmt.Errorf("failed to create stdout pipe: %w", err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
return "", fmt.Errorf("failed to create stderr pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return "", fmt.Errorf("failed to start process: %w", err)
}
process := &Process{
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
GroupID: groupID,
Cmd: cmd,
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
CreatedAt: time.Now(),
}
s.mu.Lock()
s.processes[process.ID] = process
if groupID != "" {
s.groups[groupID] = append(s.groups[groupID], process.ID)
}
s.mu.Unlock()
xlog.Debug("Successfully started process", "id", process.ID, "pid", cmd.Process.Pid)
return process.ID, nil
}
// StopProcess stops a running process
func (s *Server) StopProcess(id string) error {
s.mu.Lock()
process, exists := s.processes[id]
if !exists {
s.mu.Unlock()
return fmt.Errorf("process not found: %s", id)
}
xlog.Debug("Stopping process", "processID", id, "pid", process.Cmd.Process.Pid)
// Remove from group if it exists
if process.GroupID != "" {
groupProcesses := s.groups[process.GroupID]
for i, pid := range groupProcesses {
if pid == id {
s.groups[process.GroupID] = append(groupProcesses[:i], groupProcesses[i+1:]...)
break
}
}
if len(s.groups[process.GroupID]) == 0 {
delete(s.groups, process.GroupID)
}
}
delete(s.processes, id)
s.mu.Unlock()
if err := process.Cmd.Process.Kill(); err != nil {
xlog.Debug("Failed to kill process", "processID", id, "pid", process.Cmd.Process.Pid, "error", err)
return fmt.Errorf("failed to kill process: %w", err)
}
xlog.Debug("Successfully killed process", "processID", id, "pid", process.Cmd.Process.Pid)
return nil
}
// StopGroup stops all processes in a group
func (s *Server) StopGroup(groupID string) error {
s.mu.Lock()
processIDs, exists := s.groups[groupID]
if !exists {
s.mu.Unlock()
return fmt.Errorf("group not found: %s", groupID)
}
s.mu.Unlock()
for _, pid := range processIDs {
if err := s.StopProcess(pid); err != nil {
return fmt.Errorf("failed to stop process %s in group %s: %w", pid, groupID, err)
}
}
return nil
}
// GetGroupProcesses returns all processes in a group
func (s *Server) GetGroupProcesses(groupID string) ([]*Process, error) {
s.mu.RLock()
processIDs, exists := s.groups[groupID]
if !exists {
s.mu.RUnlock()
return nil, fmt.Errorf("group not found: %s", groupID)
}
processes := make([]*Process, 0, len(processIDs))
for _, pid := range processIDs {
if process, exists := s.processes[pid]; exists {
processes = append(processes, process)
}
}
s.mu.RUnlock()
return processes, nil
}
// ListGroups returns all group IDs
func (s *Server) ListGroups() []string {
s.mu.RLock()
defer s.mu.RUnlock()
groups := make([]string, 0, len(s.groups))
for groupID := range s.groups {
groups = append(groups, groupID)
}
return groups
}
// GetProcess returns a process by ID
func (s *Server) GetProcess(id string) (*Process, error) {
s.mu.RLock()
process, exists := s.processes[id]
s.mu.RUnlock()
if !exists {
return nil, fmt.Errorf("process not found: %s", id)
}
return process, nil
}
// ListProcesses returns all running processes
func (s *Server) ListProcesses() []*Process {
s.mu.RLock()
defer s.mu.RUnlock()
processes := make([]*Process, 0, len(s.processes))
for _, p := range s.processes {
processes = append(processes, p)
}
return processes
}
// RunProcess executes a command and returns its output
func (s *Server) RunProcess(ctx context.Context, command string, args []string, env []string) (string, error) {
cmd := exec.CommandContext(ctx, command, args...)
if len(env) > 0 {
cmd.Env = append(os.Environ(), env...)
}
output, err := cmd.CombinedOutput()
if err != nil {
return string(output), fmt.Errorf("process failed: %w", err)
}
return string(output), nil
}
// Start starts the HTTP server
func (s *Server) Start(addr string) error {
http.HandleFunc("/processes", s.handleProcesses)
http.HandleFunc("/processes/", s.handleProcess)
http.HandleFunc("/ws/", s.handleWebSocket)
http.HandleFunc("/groups", s.handleGroups)
http.HandleFunc("/groups/", s.handleGroup)
http.HandleFunc("/run", s.handleRun)
return http.ListenAndServe(addr, nil)
}
func (s *Server) handleProcesses(w http.ResponseWriter, r *http.Request) {
log.Printf("Handling /processes request: method=%s", r.Method)
switch r.Method {
case http.MethodGet:
processes := s.ListProcesses()
json.NewEncoder(w).Encode(processes)
case http.MethodPost:
var req struct {
Command string `json:"command"`
Args []string `json:"args"`
Env []string `json:"env"`
GroupID string `json:"group_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
id, err := s.StartProcess(context.Background(), req.Command, req.Args, req.Env, req.GroupID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"id": id})
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *Server) handleProcess(w http.ResponseWriter, r *http.Request) {
id := r.URL.Path[len("/processes/"):]
switch r.Method {
case http.MethodGet:
process, err := s.GetProcess(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(process)
case http.MethodDelete:
if err := s.StopProcess(id); err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.WriteHeader(http.StatusNoContent)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
id := r.URL.Path[len("/ws/"):]
xlog.Debug("Handling WebSocket connection", "processID", id)
process, err := s.GetProcess(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
if process.Cmd.ProcessState != nil && process.Cmd.ProcessState.Exited() {
xlog.Debug("Process already exited", "processID", id)
http.Error(w, "Process already exited", http.StatusGone)
return
}
xlog.Debug("Process is running", "processID", id, "pid", process.Cmd.Process.Pid)
conn, err := s.upgrader.Upgrade(w, r, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer conn.Close()
xlog.Debug("WebSocket connection established", "processID", id)
// Create a done channel to signal process completion
done := make(chan struct{})
// Handle stdin
go func() {
defer func() {
select {
case <-done:
xlog.Debug("Process stdin handler done", "processID", id)
default:
xlog.Debug("WebSocket stdin connection closed", "processID", id)
}
}()
for {
_, message, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
xlog.Debug("WebSocket stdin unexpected error", "processID", id, "error", err)
}
return
}
xlog.Debug("Received message", "processID", id, "message", string(message))
if _, err := process.Stdin.Write(message); err != nil {
if err != io.EOF {
xlog.Debug("WebSocket stdin write error", "processID", id, "error", err)
}
return
}
xlog.Debug("Message sent to process", "processID", id, "message", string(message))
}
}()
// Handle stdout and stderr
go func() {
defer func() {
select {
case <-done:
xlog.Debug("Process output handler done", "processID", id)
default:
xlog.Debug("WebSocket output connection closed", "processID", id)
}
}()
// Create a buffer for reading
buf := make([]byte, 4096)
reader := io.MultiReader(process.Stdout, process.Stderr)
for {
n, err := reader.Read(buf)
if err != nil {
if err != io.EOF {
xlog.Debug("Read error", "processID", id, "error", err)
}
return
}
if n > 0 {
xlog.Debug("Sending message", "processID", id, "size", n)
if err := conn.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
xlog.Debug("WebSocket output write error", "processID", id, "error", err)
}
return
}
xlog.Debug("Message sent to client", "processID", id, "size", n)
}
}
}()
// Wait for process to exit
xlog.Debug("Waiting for process to exit", "processID", id)
err = process.Cmd.Wait()
close(done) // Signal that the process is done
if err != nil {
xlog.Debug("Process exited with error",
"processID", id,
"pid", process.Cmd.Process.Pid,
"error", err)
} else {
xlog.Debug("Process exited successfully",
"processID", id,
"pid", process.Cmd.Process.Pid)
}
}
// Add new handlers for group management
func (s *Server) handleGroups(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
groups := s.ListGroups()
json.NewEncoder(w).Encode(groups)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *Server) handleGroup(w http.ResponseWriter, r *http.Request) {
groupID := r.URL.Path[len("/groups/"):]
switch r.Method {
case http.MethodGet:
processes, err := s.GetGroupProcesses(groupID)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(processes)
case http.MethodDelete:
if err := s.StopGroup(groupID); err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.WriteHeader(http.StatusNoContent)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *Server) handleRun(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
log.Printf("Handling /run request")
var req struct {
Command string `json:"command"`
Args []string `json:"args"`
Env []string `json:"env"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Printf("Executing one-time process: command=%s, args=%v", req.Command, req.Args)
output, err := s.RunProcess(r.Context(), req.Command, req.Args, req.Env)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Printf("One-time process completed with output length: %d", len(output))
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"output": output,
})
}
+315 -172
View File
@@ -4,6 +4,10 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"github.com/mudler/LocalAGI/core/action"
"github.com/mudler/LocalAGI/core/state"
@@ -50,6 +54,17 @@ const (
ActionSetReminder = "set_reminder"
ActionListReminders = "list_reminders"
ActionRemoveReminder = "remove_reminder"
ActionAddToMemory = "add_to_memory"
ActionListMemory = "list_memory"
ActionRemoveFromMemory = "remove_from_memory"
ActionPiKVMPowerControl = "pikvm_power_control"
ActionWebhook = "webhook"
)
const (
nameField = "name"
descriptionField = "description"
configurationField = "configuration"
)
var AvailableActions = []string{
@@ -87,14 +102,261 @@ var AvailableActions = []string{
ActionSetReminder,
ActionListReminders,
ActionRemoveReminder,
ActionAddToMemory,
ActionListMemory,
ActionRemoveFromMemory,
ActionPiKVMPowerControl,
ActionWebhook,
}
var DefaultActions = []config.FieldGroup{
{
Name: "search",
Label: "Search",
Fields: actions.SearchConfigMeta(),
},
{
Name: "browser-agent-runner",
Label: "Browser Agent Runner",
Fields: actions.BrowserAgentRunnerConfigMeta(),
},
{
Name: "deep-research-runner",
Label: "Deep Research Runner",
Fields: actions.DeepResearchRunnerConfigMeta(),
},
{
Name: "generate_image",
Label: "Generate Image",
Fields: actions.GenImageConfigMeta(),
},
{
Name: "add_to_memory",
Label: "Add to Memory",
Fields: actions.AddToMemoryConfigMeta(),
},
{
Name: "list_memory",
Label: "List Memory",
Fields: actions.ListMemoryConfigMeta(),
},
{
Name: "remove_from_memory",
Label: "Remove from Memory",
Fields: actions.RemoveFromMemoryConfigMeta(),
},
{
Name: "github-issue-labeler",
Label: "GitHub Issue Labeler",
Fields: actions.GithubIssueLabelerConfigMeta(),
},
{
Name: "github-issue-opener",
Label: "GitHub Issue Opener",
Fields: actions.GithubIssueOpenerConfigMeta(),
},
{
Name: "github-issue-editor",
Label: "GitHub Issue Editor",
Fields: actions.GithubIssueEditorConfigMeta(),
},
{
Name: "github-issue-closer",
Label: "GitHub Issue Closer",
Fields: actions.GithubIssueCloserConfigMeta(),
},
{
Name: "github-issue-commenter",
Label: "GitHub Issue Commenter",
Fields: actions.GithubIssueCommenterConfigMeta(),
},
{
Name: "github-issue-reader",
Label: "GitHub Issue Reader",
Fields: actions.GithubIssueReaderConfigMeta(),
},
{
Name: "github-issue-searcher",
Label: "GitHub Issue Search",
Fields: actions.GithubIssueSearchConfigMeta(),
},
{
Name: "github-repository-get-content",
Label: "GitHub Repository Get Content",
Fields: actions.GithubRepositoryGetContentConfigMeta(),
},
{
Name: "github-get-all-repository-content",
Label: "GitHub Get All Repository Content",
Fields: actions.GithubRepositoryGetAllContentConfigMeta(),
},
{
Name: "github-repository-search-files",
Label: "GitHub Repository Search Files",
Fields: actions.GithubRepositorySearchFilesConfigMeta(),
},
{
Name: "github-repository-list-files",
Label: "GitHub Repository List Files",
Fields: actions.GithubRepositoryListFilesConfigMeta(),
},
{
Name: "github-repository-create-or-update-content",
Label: "GitHub Repository Create/Update Content",
Fields: actions.GithubRepositoryCreateOrUpdateContentConfigMeta(),
},
{
Name: "github-readme",
Label: "GitHub Repository README",
Fields: actions.GithubRepositoryREADMEConfigMeta(),
},
{
Name: "github-pr-reader",
Label: "GitHub PR Reader",
Fields: actions.GithubPRReaderConfigMeta(),
},
{
Name: "github-pr-commenter",
Label: "GitHub PR Commenter",
Fields: actions.GithubPRCommenterConfigMeta(),
},
{
Name: "github-pr-reviewer",
Label: "GitHub PR Reviewer",
Fields: actions.GithubPRReviewerConfigMeta(),
},
{
Name: "github-pr-creator",
Label: "GitHub PR Creator",
Fields: actions.GithubPRCreatorConfigMeta(),
},
{
Name: "twitter-post",
Label: "Twitter Post",
Fields: actions.TwitterPostConfigMeta(),
},
{
Name: "send-mail",
Label: "Send Mail",
Fields: actions.SendMailConfigMeta(),
},
{
Name: "shell-command",
Label: "Shell Command",
Fields: actions.ShellConfigMeta(),
},
{
Name: "custom",
Label: "Custom",
Fields: action.CustomConfigMeta(),
},
{
Name: "scraper",
Label: "Scraper",
Fields: []config.Field{},
},
{
Name: "wikipedia",
Label: "Wikipedia",
Fields: []config.Field{},
},
{
Name: "browse",
Label: "Browse",
Fields: []config.Field{},
},
{
Name: "counter",
Label: "Counter",
Fields: []config.Field{},
},
{
Name: "call_agents",
Label: "Call Agents",
Fields: actions.CallAgentConfigMeta(),
},
{
Name: "send-telegram-message",
Label: "Send Telegram Message",
Fields: actions.SendTelegramMessageConfigMeta(),
},
{
Name: "set_reminder",
Label: "Set Reminder",
Fields: []config.Field{},
},
{
Name: "list_reminders",
Label: "List Reminders",
Fields: []config.Field{},
},
{
Name: "remove_reminder",
Label: "Remove Reminder",
Fields: []config.Field{},
},
{
Name: "pikvm_power_control",
Label: "PiKVM Power Control",
Fields: actions.PiKVMConfigMeta(),
},
{
Name: "webhook",
Label: "Webhook",
Fields: actions.WebhookConfigMeta(),
},
}
const (
ActionConfigBrowserAgentRunner = "browser-agent-runner-base-url"
ActionConfigDeepResearchRunner = "deep-research-runner-base-url"
ActionConfigSSHBoxURL = "sshbox-url"
ConfigStateDir = "state-dir"
CustomActionsDir = "custom-actions-dir"
)
func customActions(customActionsDir string, existingActionConfigs map[string]map[string]string) (allActions []types.Action) {
files, err := os.ReadDir(customActionsDir)
if err != nil {
xlog.Error("Error reading custom actions directory", "error", err)
return allActions
}
for _, file := range files {
if filepath.Ext(file.Name()) != ".go" {
continue
}
content, err := os.ReadFile(filepath.Join(customActionsDir, file.Name()))
if err != nil {
xlog.Error("Error reading custom action file", "error", err, "file", file.Name())
continue
}
actionName := strings.TrimSuffix(file.Name(), ".go")
actionConfig := map[string]string{
"name": actionName,
"description": "",
"code": string(content),
"unsafe": "false",
}
if c, exists := existingActionConfigs[actionName]; exists {
// We allow the user to customize name and description
actionConfig[descriptionField] = c[descriptionField]
actionConfig[nameField] = c[nameField]
actionConfig[configurationField] = c[configurationField]
}
a, err := Action(ActionCustom, "", actionConfig, nil, map[string]string{})
if err != nil {
xlog.Error("Error creating custom action", "error", err, "file", file.Name())
continue
}
allActions = append(allActions, a)
}
return
}
func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
return func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
return func(ctx context.Context, pool *state.AgentPool) []types.Action {
@@ -102,6 +364,7 @@ func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(c
agentName := a.Name
existingActionConfigs := map[string]map[string]string{}
for _, a := range a.Actions {
var config map[string]string
if err := json.Unmarshal([]byte(a.Config), &config); err != nil {
@@ -109,6 +372,8 @@ func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(c
continue
}
existingActionConfigs[a.Name] = config
a, err := Action(a.Name, agentName, config, pool, actionsConfigs)
if err != nil {
continue
@@ -116,6 +381,11 @@ func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(c
allActions = append(allActions, a)
}
// Now we will scan a directory for custom actions
if actionsConfigs[CustomActionsDir] != "" {
allActions = append(allActions, customActions(actionsConfigs[CustomActionsDir], existingActionConfigs)...)
}
return allActions
}
}
@@ -130,6 +400,8 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
config = map[string]string{}
}
memoryFilePath := memoryPath(agentName, actionsConfigs)
switch name {
case ActionCustom:
a, err = action.NewCustom(config, "")
@@ -183,6 +455,8 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
a = actions.NewBrowse(config)
case ActionSendMail:
a = actions.NewSendMail(config)
case ActionWebhook:
a = actions.NewWebhook(config)
case ActionTwitterPost:
a = actions.NewPostTweet(config)
case ActionCounter:
@@ -199,6 +473,14 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
a = action.NewListReminders()
case ActionRemoveReminder:
a = action.NewRemoveReminder()
case ActionAddToMemory:
a, _, _ = actions.NewMemoryActions(memoryFilePath, config)
case ActionListMemory:
_, a, _ = actions.NewMemoryActions(memoryFilePath, config)
case ActionRemoveFromMemory:
_, _, a = actions.NewMemoryActions(memoryFilePath, config)
case ActionPiKVMPowerControl:
a = actions.NewPiKVMAction(config)
default:
xlog.Error("Action not found", "name", name)
return nil, fmt.Errorf("Action not found")
@@ -211,177 +493,38 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
return a, nil
}
func ActionsConfigMeta() []config.FieldGroup {
return []config.FieldGroup{
{
Name: "search",
Label: "Search",
Fields: actions.SearchConfigMeta(),
},
{
Name: "browser-agent-runner",
Label: "Browser Agent Runner",
Fields: actions.BrowserAgentRunnerConfigMeta(),
},
{
Name: "deep-research-runner",
Label: "Deep Research Runner",
Fields: actions.DeepResearchRunnerConfigMeta(),
},
{
Name: "generate_image",
Label: "Generate Image",
Fields: actions.GenImageConfigMeta(),
},
{
Name: "github-issue-labeler",
Label: "GitHub Issue Labeler",
Fields: actions.GithubIssueLabelerConfigMeta(),
},
{
Name: "github-issue-opener",
Label: "GitHub Issue Opener",
Fields: actions.GithubIssueOpenerConfigMeta(),
},
{
Name: "github-issue-editor",
Label: "GitHub Issue Editor",
Fields: actions.GithubIssueEditorConfigMeta(),
},
{
Name: "github-issue-closer",
Label: "GitHub Issue Closer",
Fields: actions.GithubIssueCloserConfigMeta(),
},
{
Name: "github-issue-commenter",
Label: "GitHub Issue Commenter",
Fields: actions.GithubIssueCommenterConfigMeta(),
},
{
Name: "github-issue-reader",
Label: "GitHub Issue Reader",
Fields: actions.GithubIssueReaderConfigMeta(),
},
{
Name: "github-issue-searcher",
Label: "GitHub Issue Search",
Fields: actions.GithubIssueSearchConfigMeta(),
},
{
Name: "github-repository-get-content",
Label: "GitHub Repository Get Content",
Fields: actions.GithubRepositoryGetContentConfigMeta(),
},
{
Name: "github-get-all-repository-content",
Label: "GitHub Get All Repository Content",
Fields: actions.GithubRepositoryGetAllContentConfigMeta(),
},
{
Name: "github-repository-search-files",
Label: "GitHub Repository Search Files",
Fields: actions.GithubRepositorySearchFilesConfigMeta(),
},
{
Name: "github-repository-list-files",
Label: "GitHub Repository List Files",
Fields: actions.GithubRepositoryListFilesConfigMeta(),
},
{
Name: "github-repository-create-or-update-content",
Label: "GitHub Repository Create/Update Content",
Fields: actions.GithubRepositoryCreateOrUpdateContentConfigMeta(),
},
{
Name: "github-readme",
Label: "GitHub Repository README",
Fields: actions.GithubRepositoryREADMEConfigMeta(),
},
{
Name: "github-pr-reader",
Label: "GitHub PR Reader",
Fields: actions.GithubPRReaderConfigMeta(),
},
{
Name: "github-pr-commenter",
Label: "GitHub PR Commenter",
Fields: actions.GithubPRCommenterConfigMeta(),
},
{
Name: "github-pr-reviewer",
Label: "GitHub PR Reviewer",
Fields: actions.GithubPRReviewerConfigMeta(),
},
{
Name: "github-pr-creator",
Label: "GitHub PR Creator",
Fields: actions.GithubPRCreatorConfigMeta(),
},
{
Name: "twitter-post",
Label: "Twitter Post",
Fields: actions.TwitterPostConfigMeta(),
},
{
Name: "send-mail",
Label: "Send Mail",
Fields: actions.SendMailConfigMeta(),
},
{
Name: "shell-command",
Label: "Shell Command",
Fields: actions.ShellConfigMeta(),
},
{
Name: "custom",
Label: "Custom",
Fields: action.CustomConfigMeta(),
},
{
Name: "scraper",
Label: "Scraper",
Fields: []config.Field{},
},
{
Name: "wikipedia",
Label: "Wikipedia",
Fields: []config.Field{},
},
{
Name: "browse",
Label: "Browse",
Fields: []config.Field{},
},
{
Name: "counter",
Label: "Counter",
Fields: []config.Field{},
},
{
Name: "call_agents",
Label: "Call Agents",
Fields: actions.CallAgentConfigMeta(),
},
{
Name: "send-telegram-message",
Label: "Send Telegram Message",
Fields: actions.SendTelegramMessageConfigMeta(),
},
{
Name: "set_reminder",
Label: "Set Reminder",
Fields: []config.Field{},
},
{
Name: "list_reminders",
Label: "List Reminders",
Fields: []config.Field{},
},
{
Name: "remove_reminder",
Label: "Remove Reminder",
Fields: []config.Field{},
},
func ActionsConfigMeta(customActionDir string) []config.FieldGroup {
all := slices.Clone(DefaultActions)
if customActionDir != "" {
actions := customActions(customActionDir, map[string]map[string]string{})
for _, a := range actions {
all = append(all, config.FieldGroup{
Name: a.Definition().Name.String(),
Label: a.Definition().Name.String(),
Fields: []config.Field{
{
Name: nameField,
Label: "Name",
Type: config.FieldTypeText,
HelpText: "Name of the custom action",
},
{
Name: descriptionField,
Label: "Description",
Type: config.FieldTypeTextarea,
HelpText: "Description of the custom action",
},
{
Name: configurationField,
Label: "Configuration",
Type: config.FieldTypeTextarea,
HelpText: "Configuration of the custom action",
},
},
})
}
}
return all
}
+284
View File
@@ -0,0 +1,284 @@
package actions
import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"io"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/sashabaranov/go-openai/jsonschema"
)
// Remove global const and mutex, and add them as fields to a struct
type MemoryActions struct {
filePath string
customName string
customDescription string
}
type AddToMemoryAction struct{ *MemoryActions }
type ListMemoryAction struct{ *MemoryActions }
type RemoveFromMemoryAction struct{ *MemoryActions }
// NewMemoryActions returns the three actions, using the provided filePath and config
func NewMemoryActions(filePath string, config map[string]string) (*AddToMemoryAction, *ListMemoryAction, *RemoveFromMemoryAction) {
ma := &MemoryActions{filePath: filePath}
if config != nil {
ma.customName = config["custom_name"]
ma.customDescription = config["custom_description"]
}
return &AddToMemoryAction{ma}, &ListMemoryAction{ma}, &RemoveFromMemoryAction{ma}
}
type addToMemoryParams struct {
Item string `json:"item"`
}
type removeFromMemoryParams struct {
Index *int `json:"index,omitempty"`
Value string `json:"value,omitempty"`
}
func (m *MemoryActions) readMemory() ([]string, error) {
f, err := os.Open(m.filePath)
if err != nil {
if os.IsNotExist(err) {
return []string{}, nil
}
return nil, err
}
defer f.Close()
var items []string
if err := json.NewDecoder(f).Decode(&items); err != nil {
if err == io.EOF {
return []string{}, nil
}
return nil, err
}
return items, nil
}
func (m *MemoryActions) writeMemory(items []string) error {
f, err := os.Create(m.filePath)
if err != nil {
return err
}
defer f.Close()
return json.NewEncoder(f).Encode(items)
}
func (a *AddToMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
var req addToMemoryParams
if err := params.Unmarshal(&req); err != nil {
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
}
if req.Item == "" {
return types.ActionResult{}, fmt.Errorf("item cannot be empty")
}
items, err := a.readMemory()
if err != nil {
return types.ActionResult{}, err
}
items = append(items, req.Item)
if err := a.writeMemory(items); err != nil {
return types.ActionResult{}, err
}
return types.ActionResult{
Result: fmt.Sprintf("Added item to memory: %s", req.Item),
Metadata: map[string]any{"item": req.Item, "count": len(items)},
}, nil
}
func (a *ListMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
items, err := a.readMemory()
if err != nil {
return types.ActionResult{}, err
}
outputResult := "Number of items in memory: " + strconv.Itoa(len(items)) + "\n"
for i, item := range items {
outputResult += fmt.Sprintf("%d) %s\n", i, item)
}
return types.ActionResult{
Result: outputResult,
Metadata: map[string]any{"items": items},
}, nil
}
func (a *RemoveFromMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
var req removeFromMemoryParams
if err := params.Unmarshal(&req); err != nil {
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
}
items, err := a.readMemory()
if err != nil {
return types.ActionResult{}, err
}
var removed string
if req.Index != nil {
idx := *req.Index
if idx < 0 || idx >= len(items) {
return types.ActionResult{}, fmt.Errorf("index out of range")
}
removed = items[idx]
items = append(items[:idx], items[idx+1:]...)
} else if req.Value != "" {
found := false
for i, v := range items {
if v == req.Value {
removed = v
items = append(items[:i], items[i+1:]...)
found = true
break
}
}
if !found {
return types.ActionResult{}, fmt.Errorf("value not found in memory")
}
} else {
return types.ActionResult{}, fmt.Errorf("must provide index or value to remove")
}
if err := a.writeMemory(items); err != nil {
return types.ActionResult{}, err
}
return types.ActionResult{
Result: fmt.Sprintf("Removed item from memory: %s", removed),
Metadata: map[string]any{"removed": removed, "count": len(items)},
}, nil
}
func (a *AddToMemoryAction) Definition() types.ActionDefinition {
name := "add_to_memory"
description := "Add a string item to memory (stored in a JSON file)."
if a.customName != "" {
name = a.customName
}
if a.customDescription != "" {
description = a.customDescription
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(name),
Description: description,
Properties: map[string]jsonschema.Definition{
"item": {
Type: jsonschema.String,
Description: "The string item to add to memory.",
},
},
Required: []string{"item"},
}
}
func (a *ListMemoryAction) Definition() types.ActionDefinition {
name := "list_memory"
description := "List all items currently stored in memory."
if a.customName != "" {
name = a.customName
}
if a.customDescription != "" {
description = a.customDescription
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(name),
Description: description,
Properties: map[string]jsonschema.Definition{},
Required: []string{},
}
}
func (a *RemoveFromMemoryAction) Definition() types.ActionDefinition {
name := "remove_from_memory"
description := "Remove an item from memory by index or value."
if a.customName != "" {
name = a.customName
}
if a.customDescription != "" {
description = a.customDescription
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(name),
Description: description,
Properties: map[string]jsonschema.Definition{
"index": {
Type: jsonschema.Integer,
Description: "The index of the item to remove (optional, 0-based)",
},
"value": {
Type: jsonschema.String,
Description: "The value of the item to remove (optional)",
},
},
Required: []string{},
}
}
func (a *AddToMemoryAction) Plannable() bool { return true }
func (a *ListMemoryAction) Plannable() bool { return true }
func (a *RemoveFromMemoryAction) Plannable() bool { return true }
// AddToMemoryConfigMeta returns the metadata for AddToMemory action configuration fields
func AddToMemoryConfigMeta() []config.Field {
return []config.Field{
{
Name: "custom_name",
Label: "Custom Name",
Type: config.FieldTypeText,
Required: false,
HelpText: "Custom name for the action (optional, defaults to 'add_to_memory')",
},
{
Name: "custom_description",
Label: "Custom Description",
Type: config.FieldTypeText,
Required: false,
HelpText: "Custom description for the action (optional, defaults to 'Add a string item to memory (stored in a JSON file).')",
},
}
}
// ListMemoryConfigMeta returns the metadata for ListMemory action configuration fields
func ListMemoryConfigMeta() []config.Field {
return []config.Field{
{
Name: "custom_name",
Label: "Custom Name",
Type: config.FieldTypeText,
Required: false,
HelpText: "Custom name for the action (optional, defaults to 'list_memory')",
},
{
Name: "custom_description",
Label: "Custom Description",
Type: config.FieldTypeText,
Required: false,
HelpText: "Custom description for the action (optional, defaults to 'List all items currently stored in memory.')",
},
}
}
// RemoveFromMemoryConfigMeta returns the metadata for RemoveFromMemory action configuration fields
func RemoveFromMemoryConfigMeta() []config.Field {
return []config.Field{
{
Name: "custom_name",
Label: "Custom Name",
Type: config.FieldTypeText,
Required: false,
HelpText: "Custom name for the action (optional, defaults to 'remove_from_memory')",
},
{
Name: "custom_description",
Label: "Custom Description",
Type: config.FieldTypeText,
Required: false,
HelpText: "Custom description for the action (optional, defaults to 'Remove an item from memory by index or value.')",
},
}
}
+83
View File
@@ -0,0 +1,83 @@
package actions_test
import (
"context"
"os"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/services/actions"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("MemoryActions", func() {
var (
tmpFile string
aAdd *actions.AddToMemoryAction
aList *actions.ListMemoryAction
aRemove *actions.RemoveFromMemoryAction
)
BeforeEach(func() {
f, err := os.CreateTemp("", "memory_test_*.json")
Expect(err).ToNot(HaveOccurred())
tmpFile = f.Name()
f.Close()
aAdd, aList, aRemove = actions.NewMemoryActions(tmpFile, map[string]string{})
})
AfterEach(func() {
os.Remove(tmpFile)
})
It("adds and lists items", func() {
_, err := aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
Expect(err).ToNot(HaveOccurred())
_, err = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "bar"})
Expect(err).ToNot(HaveOccurred())
res, err := aList.Run(context.TODO(), nil, types.ActionParams{})
Expect(err).ToNot(HaveOccurred())
Expect(res.Metadata["items"]).To(ContainElements("foo", "bar"))
})
It("removes by index", func() {
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "bar"})
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"index": 0})
Expect(err).ToNot(HaveOccurred())
res, _ := aList.Run(context.TODO(), nil, types.ActionParams{})
Expect(res.Metadata["items"]).To(ConsistOf("bar"))
})
It("removes by value", func() {
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "bar"})
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"value": "bar"})
Expect(err).ToNot(HaveOccurred())
res, _ := aList.Run(context.TODO(), nil, types.ActionParams{})
Expect(res.Metadata["items"]).To(ConsistOf("foo"))
})
It("returns error for out of range index", func() {
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"index": 2})
Expect(err).To(HaveOccurred())
})
It("returns error for value not found", func() {
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"value": "bar"})
Expect(err).To(HaveOccurred())
})
It("returns error for empty item", func() {
_, err := aAdd.Run(context.TODO(), nil, types.ActionParams{"item": ""})
Expect(err).To(HaveOccurred())
})
It("returns error if neither index nor value provided", func() {
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{})
Expect(err).To(HaveOccurred())
})
})
+199
View File
@@ -0,0 +1,199 @@
package actions
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/sashabaranov/go-openai/jsonschema"
)
func NewPiKVMAction(config map[string]string) *PiKVMAction {
return &PiKVMAction{
hostname: config["hostname"],
username: config["username"],
password: config["password"],
customName: config["custom_name"],
customDescription: config["custom_description"],
insecure: config["insecure"] == "true",
}
}
type PiKVMAction struct {
hostname string
username string
password string
customName string
customDescription string
insecure bool
}
type pikvmPowerParams struct {
Action string `json:"action"`
}
func (a *PiKVMAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
var req pikvmPowerParams
if err := params.Unmarshal(&req); err != nil {
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
}
// Validate action parameter
validActions := map[string]bool{
"on": true,
"off": true,
"off_hard": true,
"reset_hard": true,
}
if !validActions[req.Action] {
return types.ActionResult{}, fmt.Errorf("invalid action: %s. Valid actions are: on, off, off_hard, reset_hard", req.Action)
}
// Check if required config is provided
if a.hostname == "" {
return types.ActionResult{}, fmt.Errorf("hostname is required in action configuration")
}
if a.username == "" {
return types.ActionResult{}, fmt.Errorf("username is required in action configuration")
}
if a.password == "" {
return types.ActionResult{}, fmt.Errorf("password is required in action configuration")
}
// Build the API URL
apiURL := fmt.Sprintf("https://%s/api/atx/power", a.hostname)
insecure := false
if a.insecure {
insecure = true
}
// Create HTTP client with basic auth
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
}
client := &http.Client{Transport: tr}
reqHTTP, err := http.NewRequestWithContext(ctx, "POST", apiURL, nil)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to create HTTP request: %w", err)
}
// Set basic authentication
reqHTTP.SetBasicAuth(a.username, a.password)
// Add query parameters
q := reqHTTP.URL.Query()
q.Add("action", req.Action)
reqHTTP.URL.RawQuery = q.Encode()
// Make the request
resp, err := client.Do(reqHTTP)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to make HTTP request to PiKVM: %w", err)
}
defer resp.Body.Close()
// Check response status
if resp.StatusCode != http.StatusOK {
return types.ActionResult{}, fmt.Errorf("PiKVM API returned status %d: %s", resp.StatusCode, resp.Status)
}
// Determine action description for user-friendly response
actionDesc := map[string]string{
"on": "power on",
"off": "power off",
"off_hard": "hard power off",
"reset_hard": "hard reset",
}
result := fmt.Sprintf("Successfully sent %s command to PiKVM at %s", actionDesc[req.Action], a.hostname)
return types.ActionResult{
Result: result,
Metadata: map[string]any{
"action": req.Action,
"hostname": a.hostname,
"status": "success",
},
}, nil
}
func (a *PiKVMAction) Definition() types.ActionDefinition {
name := "pikvm_power_control"
description := "Control power state of a PiKVM device using ATX power management."
if a.customName != "" {
name = a.customName
}
if a.customDescription != "" {
description = a.customDescription
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(name),
Description: description,
Properties: map[string]jsonschema.Definition{
"action": {
Type: jsonschema.String,
Description: "The power action to perform on the PiKVM device.",
Enum: []string{"on", "off", "off_hard", "reset_hard"},
},
},
Required: []string{"action"},
}
}
func (a *PiKVMAction) Plannable() bool {
return true
}
// PiKVMConfigMeta returns the metadata for PiKVM action configuration fields
func PiKVMConfigMeta() []config.Field {
return []config.Field{
{
Name: "hostname",
Label: "PiKVM Hostname",
Type: config.FieldTypeText,
Required: true,
HelpText: "The hostname or IP address of the PiKVM device (e.g., pikvm.local or 192.168.1.100)",
},
{
Name: "username",
Label: "Username",
Type: config.FieldTypeText,
Required: true,
HelpText: "Username for PiKVM authentication (usually 'admin')",
},
{
Name: "password",
Label: "Password",
Type: config.FieldTypeText,
Required: true,
HelpText: "Password for PiKVM authentication",
},
{
Name: "custom_name",
Label: "Custom Action Name",
Type: config.FieldTypeText,
Required: false,
HelpText: "Custom name for this action (optional, defaults to 'pikvm_power_control')",
},
{
Name: "custom_description",
Label: "Custom Description",
Type: config.FieldTypeText,
Required: false,
HelpText: "Custom description for this action (optional)",
},
{
Name: "insecure",
Label: "Insecure",
Type: config.FieldTypeCheckbox,
Required: false,
HelpText: "Skip certificate verification (optional)",
},
}
}
+256
View File
@@ -0,0 +1,256 @@
// Package actions contains action implementations used by LocalAGI.
// This file implements the "webhook" action which can send an HTTP request
// to an external service with a configurable method, content type, and payload.
package actions
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"strings"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/sashabaranov/go-openai/jsonschema"
)
// NewWebhook constructs a WebhookAction using provided configuration values:
// - url: Destination endpoint for the HTTP request (required).
// - method: HTTP method to use (GET, POST, PUT, DELETE, ...). Defaults to POST.
// - contentType: Value for the Content-Type header (e.g., application/json).
// - payloadTemplate: Optional template for the request body; the runtime parameter
// "payload" (if provided) will replace the "{{payload}}" placeholder inside this template.
func NewWebhook(cfg map[string]string) *WebhookAction {
wa := &WebhookAction{
url: strings.TrimSpace(cfg["url"]),
method: strings.ToUpper(strings.TrimSpace(cfg["method"])),
contentType: strings.TrimSpace(cfg["contentType"]),
payloadTemplate: cfg["payloadTemplate"],
}
// Optional custom overrides
if cfg != nil {
wa.customName = cfg["custom_name"]
wa.customDescription = cfg["custom_description"]
wa.customPayloadDescription = cfg["custom_payload_description"]
}
if wa.method == "" {
wa.method = http.MethodPost
}
return wa
}
// WebhookAction holds the static configuration for the webhook.
// These values come from the action configuration (UI/agent config),
// while the runtime parameter only carries the dynamic payload.
// - url: Target endpoint for the request.
// - method: HTTP method to use. Defaults to POST if not provided.
// - contentType: Sets the Content-Type header when a body is sent.
// - payloadTemplate: Optional template used to build the request body; occurrences
// of "{{payload}}" get replaced with the runtime payload string.
// If no placeholder is present, the template is used as-is.
// For GET requests the body is omitted regardless of payload.
//
// Note: This action does not follow redirects!
type WebhookAction struct {
url string
method string
contentType string
payloadTemplate string
customName string
customDescription string
customPayloadDescription string
}
// Run executes the webhook call.
// It reads the runtime parameter "payload" (optional), merges it into the
// configured payloadTemplate (if any), constructs an HTTP request using the
// configured URL, method and content type, and then returns a summary with the
// response status and body (truncated to 4KiB for safety).
func (a *WebhookAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
// Runtime parameters: only payload
type input struct {
Payload string `json:"payload"`
}
var in input
if err := params.Unmarshal(&in); err != nil {
return types.ActionResult{}, err
}
// Validate essential configuration. The URL must be provided via the
// action configuration (not via runtime parameters).
if a.url == "" {
return types.ActionResult{}, fmt.Errorf("configuration.url is required")
}
method := a.method
// Build the request body based on template and payload:
// - If a payloadTemplate is provided, replace occurrences of "{{payload}}"
// with the runtime payload value.
// - If the template does not contain the placeholder but is provided, we use
// the template as-is (common for static JSON bodies prepared at config time).
// - If no template is configured, we send the runtime payload as-is.
// - For GET requests the body is omitted regardless of payload.
var payload string
if a.payloadTemplate != "" {
payload = strings.ReplaceAll(a.payloadTemplate, "{{payload}}", in.Payload)
if payload == a.payloadTemplate && in.Payload != "" {
// If no placeholder found, fallback to template or payload alone
payload = a.payloadTemplate
}
} else {
payload = in.Payload
}
var body io.Reader
if method != http.MethodGet && payload != "" {
body = bytes.NewBufferString(payload)
}
// Create the HTTP request bound to the provided context so that cancellation
// or timeouts from the caller propagate to the outbound call.
req, err := http.NewRequestWithContext(ctx, method, a.url, body)
if err != nil {
return types.ActionResult{}, err
}
// Set Content-Type header if configured. For GET requests this header is
// typically ignored by servers as there is no body.
if a.contentType != "" {
req.Header.Set("Content-Type", a.contentType)
}
// Use a new http.Client with default settings. Consider configuring timeouts
// at the caller level via the context, or wiring a custom client if needed.
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return types.ActionResult{}, err
}
defer resp.Body.Close()
// Read and safely truncate the response body to avoid flooding the agent's
// context with very large payloads. Errors on ReadAll are ignored here as
// we already have the status code.
respBytes, _ := io.ReadAll(resp.Body)
respBody := string(respBytes)
if len(respBody) > 4096 {
respBody = respBody[:4096] + "... (truncated)"
}
return types.ActionResult{
// Return the response body as the result.
// If the response body is empty, use the status text as the result (e.g. "OK" for status code 200).
Result: func() string {
if respBody == "" {
return http.StatusText(resp.StatusCode)
}
return respBody
}(),
// Include the response status code in the metadata.
Metadata: map[string]interface{}{
"statusCode": resp.StatusCode,
},
}, nil
}
// Definition returns the action schema exposed to the planner/runtime.
// Only the runtime parameter "payload" is accepted; all connection details
// are configured statically via the action configuration UI.
func (a *WebhookAction) Definition() types.ActionDefinition {
name := "webhook"
description := "Send an HTTP request to a configured URL/method/content-type. Accepts a runtime payload parameter optionally inserted into the configured payload template."
if a.customName != "" {
name = a.customName
}
if a.customDescription != "" {
description = a.customDescription
}
payloadDesc := "Payload/body to send with the request at runtime. If a payloadTemplate is configured, '{{payload}}' will be replaced by this value."
if a.customPayloadDescription != "" {
payloadDesc = a.customPayloadDescription
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(name),
Description: description,
Properties: map[string]jsonschema.Definition{
"payload": {
Type: jsonschema.String,
Description: payloadDesc,
},
},
}
}
// Plannable indicates the action can be suggested/used by planners without
// requiring hidden context; inputs are straightforward and safe.
func (a *WebhookAction) Plannable() bool { return true }
// WebhookConfigMeta returns the metadata for Webhook action configuration fields:
// - url: The endpoint to send requests to (required).
// - method: One of GET/POST/PUT/DELETE. Defaults to POST.
// - contentType: Common content types selectable from a dropdown.
// - payloadTemplate: Optional body template. At runtime, "{{payload}}" is
// replaced by the provided payload parameter. If missing, the template is used
// as-is; for GET, no body is sent regardless.
func WebhookConfigMeta() []config.Field {
return []config.Field{
{
Name: "custom_name",
Label: "Custom Name",
Type: config.FieldTypeText,
Required: false,
HelpText: "Custom name for the action (optional, defaults to 'webhook')",
},
{
Name: "custom_description",
Label: "Custom Description",
Type: config.FieldTypeText,
Required: false,
HelpText: "Custom description for the action (optional)",
},
{
Name: "custom_payload_description",
Label: "Custom Payload Description",
Type: config.FieldTypeText,
Required: false,
HelpText: "Override the payload parameter description shown in the UI/schema (optional).",
},
{
Name: "url",
Label: "URL",
Type: config.FieldTypeText,
Required: true,
HelpText: "Destination URL for the webhook",
},
{
Name: "method",
Label: "HTTP Method",
Type: config.FieldTypeSelect,
Options: []config.FieldOption{{Value: http.MethodGet, Label: "GET"}, {Value: http.MethodPost, Label: "POST"}, {Value: http.MethodPut, Label: "PUT"}, {Value: http.MethodDelete, Label: "DELETE"}},
DefaultValue: http.MethodPost,
Required: true,
HelpText: "HTTP method to use",
},
{
Name: "contentType",
Label: "Content Type",
Type: config.FieldTypeSelect,
Options: []config.FieldOption{
{Value: "application/json", Label: "application/json"},
{Value: "text/plain", Label: "text/plain"},
{Value: "application/x-www-form-urlencoded", Label: "application/x-www-form-urlencoded"},
},
Required: true,
HelpText: "Content-Type header to send",
},
{
Name: "payloadTemplate",
Label: "Payload Template",
Type: config.FieldTypeTextarea,
HelpText: "Optional template used to craft the request body. Use '{{payload}}' as placeholder for the runtime payload.",
},
}
}
+28
View File
@@ -0,0 +1,28 @@
package services
import (
"fmt"
"os"
"github.com/mudler/LocalAGI/pkg/xlog"
)
func memoryPath(agentName string, actionsConfigs map[string]string) string {
// Compose memory file path based on stateDir and agentName, using a subdirectory
memoryFilePath := "memory.json"
if actionsConfigs != nil {
if stateDir, ok := actionsConfigs[ConfigStateDir]; ok && stateDir != "" {
memoryDir := fmt.Sprintf("%s/memory", stateDir)
err := os.MkdirAll(memoryDir, 0755) // ensure the directory exists
if err != nil {
xlog.Error("Error creating memory directory", "error", err)
return memoryFilePath
}
memoryFilePath = fmt.Sprintf("%s/%s.json", memoryDir, agentName)
} else {
memoryFilePath = fmt.Sprintf("%s.memory.json", agentName)
}
}
return memoryFilePath
}
+213 -42
View File
@@ -56,14 +56,24 @@ func (t *Telegram) isBotMentioned(message string, botUsername string) bool {
}
func (t *Telegram) chatFromMessage(update *models.Update) (openai.ChatCompletionMessage, error) {
if len(update.Message.Photo) == 0 {
return openai.ChatCompletionMessage{
Content: update.Message.Text,
Role: "user",
}, nil
// Handle audio messages
if update.Message.Voice != nil || update.Message.Audio != nil {
return t.handleAudioMessage(update)
}
// Handle photo messages
if len(update.Message.Photo) > 0 {
return t.handlePhotoMessage(update)
}
// Handle text messages
return openai.ChatCompletionMessage{
Content: update.Message.Text,
Role: "user",
}, nil
}
func (t *Telegram) handlePhotoMessage(update *models.Update) (openai.ChatCompletionMessage, error) {
xlog.Debug("Image", "found image")
// Get the largest photo
photo := update.Message.Photo[len(update.Message.Photo)-1]
@@ -77,44 +87,122 @@ func (t *Telegram) chatFromMessage(update *models.Update) (openai.ChatCompletion
})
if err != nil {
xlog.Error("Error getting file", "error", err)
} else {
// Construct the full URL for downloading the file
fileURL := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", t.Token, file.FilePath)
// Download the file content
resp, err := http.Get(fileURL)
if err != nil {
xlog.Error("Error downloading file", "error", err)
} else {
defer resp.Body.Close()
imageBytes, err := io.ReadAll(resp.Body)
if err != nil {
xlog.Error("Error reading image", "error", err)
} else {
// Encode to base64
imgBase64 := base64.StdEncoding.EncodeToString(imageBytes)
xlog.Debug("Image", "sending encoded image")
// Add to conversation as multi-content message
return openai.ChatCompletionMessage{
Role: "user",
MultiContent: []openai.ChatMessagePart{
{
Text: update.Message.Caption,
Type: openai.ChatMessagePartTypeText,
},
{
Type: openai.ChatMessagePartTypeImageURL,
ImageURL: &openai.ChatMessageImageURL{
URL: fmt.Sprintf("data:image/jpeg;base64,%s", imgBase64),
},
},
},
}, nil
}
}
return openai.ChatCompletionMessage{}, err
}
return openai.ChatCompletionMessage{}, errors.New("no image found")
// Construct the full URL for downloading the file
fileURL := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", t.Token, file.FilePath)
// Download the file content
resp, err := http.Get(fileURL)
if err != nil {
xlog.Error("Error downloading file", "error", err)
return openai.ChatCompletionMessage{}, err
}
defer resp.Body.Close()
imageBytes, err := io.ReadAll(resp.Body)
if err != nil {
xlog.Error("Error reading image", "error", err)
return openai.ChatCompletionMessage{}, err
}
// Encode to base64
imgBase64 := base64.StdEncoding.EncodeToString(imageBytes)
xlog.Debug("Image", "sending encoded image")
// Add to conversation as multi-content message
return openai.ChatCompletionMessage{
Role: "user",
MultiContent: []openai.ChatMessagePart{
{
Text: update.Message.Caption,
Type: openai.ChatMessagePartTypeText,
},
{
Type: openai.ChatMessagePartTypeImageURL,
ImageURL: &openai.ChatMessageImageURL{
URL: fmt.Sprintf("data:image/jpeg;base64,%s", imgBase64),
},
},
},
}, nil
}
func (t *Telegram) handleAudioMessage(update *models.Update) (openai.ChatCompletionMessage, error) {
var fileID string
var audioType string
if update.Message.Voice != nil {
fileID = update.Message.Voice.FileID
audioType = "voice"
} else if update.Message.Audio != nil {
fileID = update.Message.Audio.FileID
audioType = "audio"
}
xlog.Debug("Audio message received", "type", audioType, "fileID", fileID)
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// Download the audio file
file, err := t.bot.GetFile(ctx, &bot.GetFileParams{
FileID: fileID,
})
if err != nil {
xlog.Error("Error getting audio file", "error", err)
return openai.ChatCompletionMessage{}, err
}
// Construct the full URL for downloading the file
fileURL := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", t.Token, file.FilePath)
// Download the file content
resp, err := http.Get(fileURL)
if err != nil {
xlog.Error("Error downloading audio file", "error", err)
return openai.ChatCompletionMessage{}, err
}
defer resp.Body.Close()
audioBytes, err := io.ReadAll(resp.Body)
if err != nil {
xlog.Error("Error reading audio file", "error", err)
return openai.ChatCompletionMessage{}, err
}
// Create a temporary file for transcription
tempFile, err := os.CreateTemp("", "telegram_audio_*.ogg")
if err != nil {
xlog.Error("Error creating temp file", "error", err)
return openai.ChatCompletionMessage{}, err
}
defer os.Remove(tempFile.Name())
// Write audio data to temp file
if _, err := tempFile.Write(audioBytes); err != nil {
tempFile.Close()
xlog.Error("Error writing audio to temp file", "error", err)
return openai.ChatCompletionMessage{}, err
}
tempFile.Close()
// Transcribe the audio using the agent's Transcribe method
transcription, err := t.agent.Transcribe(ctx, tempFile.Name())
if err != nil {
xlog.Error("Error transcribing audio", "error", err)
return openai.ChatCompletionMessage{
Content: fmt.Sprintf("I received an audio message but couldn't transcribe it: %v", err),
Role: "user",
}, nil
}
xlog.Debug("Audio transcribed successfully", "transcription", transcription)
return openai.ChatCompletionMessage{
Content: transcription,
Role: "user",
}, nil
}
// handleGroupMessage handles messages in group chats
@@ -176,6 +264,11 @@ func (t *Telegram) handleGroupMessage(ctx context.Context, b *bot.Bot, a *agent.
"chatID": update.Message.Chat.ID,
}
// Track if the original message was audio for TTS response
if update.Message.Voice != nil || update.Message.Audio != nil {
metadata["originalMessageType"] = "audio"
}
chatMessage, err := t.chatFromMessage(update)
if err != nil {
xlog.Error("Error extracting chat message", "error", err)
@@ -251,6 +344,34 @@ func (t *Telegram) handleGroupMessage(ctx context.Context, b *bot.Bot, a *agent.
xlog.Error("Error handling multimedia content", "error", err)
}
// Check if original message was audio and generate TTS response
if metadata["originalMessageType"] == "audio" && res.Response != "" {
xlog.Debug("Original message was audio, generating TTS response")
audioData, err := t.agent.TTS(ctx, res.Response)
if err != nil {
xlog.Error("Error generating TTS", "error", err)
} else {
// Send audio response
err = sendAudioToTelegram(ctx, t.bot, update.Message.Chat.ID, audioData, res.Response)
if err != nil {
xlog.Error("Error sending audio response", "error", err)
} else {
xlog.Debug("Audio response sent successfully")
// Remove the thinking placeholder message before returning
_, err := t.bot.DeleteMessage(ctx, &bot.DeleteMessageParams{
ChatID: update.Message.Chat.ID,
MessageID: msg.ID,
})
if err != nil {
xlog.Error("Error deleting thinking placeholder", "error", err)
}
// Don't send text response if audio was sent successfully
return
}
}
}
// Update the message with the final response
formattedResponse := formatResponseWithURLs(res.Response, urls)
@@ -396,6 +517,24 @@ func sendImageToTelegram(ctx context.Context, b *bot.Bot, chatID int64, url stri
return nil
}
// sendAudioToTelegram sends audio data to Telegram
func sendAudioToTelegram(ctx context.Context, b *bot.Bot, chatID int64, audioData []byte, caption string) error {
// Send audio with caption
_, err := b.SendVoice(ctx, &bot.SendVoiceParams{
ChatID: chatID,
Voice: &models.InputFileUpload{
Filename: "response.mp3",
Data: bytes.NewReader(audioData),
},
Caption: caption,
})
if err != nil {
return fmt.Errorf("error sending audio: %w", err)
}
return nil
}
// handleMultimediaContent processes and sends multimedia content from the agent's response
func (t *Telegram) handleMultimediaContent(ctx context.Context, chatID int64, res *types.JobResult) ([]string, error) {
var urls []string
@@ -540,6 +679,11 @@ func (t *Telegram) handleUpdate(ctx context.Context, b *bot.Bot, a *agent.Agent,
"chatID": update.Message.Chat.ID,
}
// Track if the original message was audio for TTS response
if update.Message.Voice != nil || update.Message.Audio != nil {
metadata["originalMessageType"] = "audio"
}
// Create a new job with the conversation history and metadata
job := types.NewJob(
types.WithConversationHistory(currentConv),
@@ -603,6 +747,33 @@ func (t *Telegram) handleUpdate(ctx context.Context, b *bot.Bot, a *agent.Agent,
xlog.Error("Error handling multimedia content", "error", err)
}
// Check if original message was audio and generate TTS response
if metadata["originalMessageType"] == "audio" && res.Response != "" {
xlog.Debug("Original message was audio, generating TTS response")
audioData, err := t.agent.TTS(ctx, res.Response)
if err != nil {
xlog.Error("Error generating TTS", "error", err)
} else {
// Send audio response
err = sendAudioToTelegram(ctx, t.bot, update.Message.Chat.ID, audioData, res.Response)
if err != nil {
xlog.Error("Error sending audio response", "error", err)
} else {
xlog.Debug("Audio response sent successfully")
// Remove the thinking placeholder message before returning
_, err := t.bot.DeleteMessage(ctx, &bot.DeleteMessageParams{
ChatID: update.Message.Chat.ID,
MessageID: msg.ID,
})
if err != nil {
xlog.Error("Error deleting thinking placeholder", "error", err)
}
// Don't send text response if audio was sent successfully
return
}
}
}
// Update the message with the final response
formattedResponse := formatResponseWithURLs(res.Response, urls)
+129 -20
View File
@@ -1,10 +1,15 @@
package services
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/LocalAGI/services/actions"
"github.com/mudler/LocalAGI/services/prompts"
"github.com/mudler/LocalAGI/core/agent"
@@ -13,36 +18,140 @@ import (
const (
DynamicPromptCustom = "custom"
DynamicPromptMemory = "memory"
)
var AvailableBlockPrompts = []string{
DynamicPromptCustom,
DynamicPromptMemory,
}
func DynamicPromptsConfigMeta() []config.FieldGroup {
return []config.FieldGroup{
prompts.NewDynamicPromptConfigMeta(),
type dynamicPrompt struct {
agent.DynamicPrompt
Name string
}
func dynamicPrompts(customDirectory string, existingConfigs map[string]map[string]string) (allPrompts []dynamicPrompt) {
files, err := os.ReadDir(customDirectory)
if err != nil {
xlog.Error("Error reading custom actions directory", "error", err)
return
}
}
func DynamicPrompts(a *state.AgentConfig) []agent.DynamicPrompt {
promptblocks := []agent.DynamicPrompt{}
for _, c := range a.DynamicPrompts {
var config map[string]string
if err := json.Unmarshal([]byte(c.Config), &config); err != nil {
xlog.Info("Error unmarshalling connector config", err)
for _, file := range files {
if filepath.Ext(file.Name()) != ".go" {
continue
}
switch c.Type {
case DynamicPromptCustom:
prompt, err := prompts.NewDynamicPrompt(config, "")
if err != nil {
xlog.Error("Error creating custom prompt", "error", err)
continue
}
promptblocks = append(promptblocks, prompt)
content, err := os.ReadFile(filepath.Join(customDirectory, file.Name()))
if err != nil {
xlog.Error("Error reading custom action file", "error", err, "file", file.Name())
continue
}
dynamicPromptName := strings.TrimSuffix(file.Name(), ".go")
dynamicPromptConfig := map[string]string{
"name": dynamicPromptName,
"code": string(content),
}
if c, exists := existingConfigs[dynamicPromptName]; exists {
dynamicPromptConfig["configuration"] = c["configuration"]
}
a, err := prompts.NewDynamicCustomPrompt(dynamicPromptConfig, "")
if err != nil {
xlog.Error("Error creating custom dynamic prompt", "error", err, "file", file.Name())
continue
}
if !a.CanRender() {
continue
}
allPrompts = append(allPrompts, dynamicPrompt{
DynamicPrompt: a,
Name: dynamicPromptName,
})
}
return
}
func DynamicPromptsConfigMeta(customDirectory string) []config.FieldGroup {
defaultDynamicPrompts := []config.FieldGroup{
prompts.NewDynamicPromptConfigMeta(),
prompts.NewMemoryPromptConfigMeta(),
}
if customDirectory != "" {
prompts := dynamicPrompts(customDirectory, map[string]map[string]string{})
for _, p := range prompts {
defaultDynamicPrompts = append(defaultDynamicPrompts, config.FieldGroup{
Name: p.Name,
Label: p.Name,
Fields: []config.Field{
{
Name: "configuration",
Label: "Configuration",
Type: config.FieldTypeTextarea,
HelpText: "Configuration for the custom prompt",
},
},
})
}
}
return defaultDynamicPrompts
}
func DynamicPrompts(dynamicConfig map[string]string) func(*state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []agent.DynamicPrompt {
return func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []agent.DynamicPrompt {
return func(ctx context.Context, pool *state.AgentPool) []agent.DynamicPrompt {
customDirectory := dynamicConfig[CustomActionsDir]
existingDynamicPromptsConfigs := map[string]map[string]string{}
for _, c := range a.DynamicPrompts {
var config map[string]string
if err := json.Unmarshal([]byte(c.Config), &config); err != nil {
xlog.Info("Error unmarshalling connector config", err)
continue
}
existingDynamicPromptsConfigs[c.Type] = config
}
dynamicPromptsFound := dynamicPrompts(customDirectory, existingDynamicPromptsConfigs)
memoryFilePath := memoryPath(a.Name, dynamicConfig)
promptblocks := []agent.DynamicPrompt{}
for _, c := range a.DynamicPrompts {
config := existingDynamicPromptsConfigs[c.Type]
switch c.Type {
case DynamicPromptCustom:
prompt, err := prompts.NewDynamicCustomPrompt(config, "")
if err != nil {
xlog.Error("Error creating custom prompt", "error", err)
continue
}
promptblocks = append(promptblocks, prompt)
case DynamicPromptMemory:
_, memory, _ := actions.NewMemoryActions(memoryFilePath, dynamicConfig)
promptblocks = append(promptblocks,
prompts.NewMemoryPrompt(config, memory),
)
default:
// Check if we have configured a custom dynamic prompt coming from a directory
for _, p := range dynamicPromptsFound {
if p.Name == c.Type {
promptblocks = append(promptblocks, p.DynamicPrompt)
}
}
}
}
return promptblocks
}
}
return promptblocks
}
+51 -15
View File
@@ -2,23 +2,26 @@ package prompts
import (
"fmt"
"os"
"regexp"
"strings"
"github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
)
type DynamicPrompt struct {
type DynamicCustomPrompt struct {
config map[string]string
goPkgPath string
i *interp.Interpreter
}
func NewDynamicPrompt(config map[string]string, goPkgPath string) (*DynamicPrompt, error) {
a := &DynamicPrompt{
func NewDynamicCustomPrompt(config map[string]string, goPkgPath string) (*DynamicCustomPrompt, error) {
a := &DynamicCustomPrompt{
config: config,
goPkgPath: goPkgPath,
}
@@ -34,19 +37,20 @@ func NewDynamicPrompt(config map[string]string, goPkgPath string) (*DynamicPromp
return a, nil
}
func (a *DynamicPrompt) callInit() error {
func (a *DynamicCustomPrompt) callInit() error {
if a.i == nil {
return nil
}
v, err := a.i.Eval(fmt.Sprintf("%s.Init", a.config["name"]))
if err != nil {
return err
xlog.Warn("No init function found for custom prompt", "error", err, "action", a.config["name"])
return nil
}
run := v.Interface().(func() error)
run := v.Interface().(func(string) error)
return run()
return run(a.config["configuration"])
}
func NewDynamicPromptConfigMeta() config.FieldGroup {
@@ -77,16 +81,23 @@ func NewDynamicPromptConfigMeta() config.FieldGroup {
Required: false,
HelpText: "Enable if the code needs to use unsafe Go features",
},
{
Name: "configuration",
Label: "Configuration",
Type: config.FieldTypeTextarea,
HelpText: "Configuration for the custom prompt",
},
},
}
}
func (a *DynamicPrompt) initializeInterpreter() error {
func (a *DynamicCustomPrompt) initializeInterpreter() error {
if _, exists := a.config["code"]; exists && a.i == nil {
unsafe := strings.ToLower(a.config["unsafe"]) == "true"
i := interp.New(interp.Options{
GoPath: a.goPkgPath,
Unrestricted: unsafe,
Env: os.Environ(),
})
if err := i.Use(stdlib.Symbols); err != nil {
return err
@@ -96,6 +107,15 @@ func (a *DynamicPrompt) initializeInterpreter() error {
a.config["name"] = "custom"
}
// let's find first if there is already a package declarated in the code
// the user might want to specify it to not break syntax with IDEs
re := regexp.MustCompile("package (\\w+)")
packageName := re.FindStringSubmatch(a.config["code"])
if len(packageName) > 1 {
// remove it from the code, normalize to `name`
a.config["code"] = re.ReplaceAllString(a.config["code"], "")
}
_, err := i.Eval(fmt.Sprintf("package %s\n%s", a.config["name"], a.config["code"]))
if err != nil {
return err
@@ -107,18 +127,34 @@ func (a *DynamicPrompt) initializeInterpreter() error {
return nil
}
func (a *DynamicPrompt) Render(c *agent.Agent) (string, error) {
v, err := a.i.Eval(fmt.Sprintf("%s.Render", a.config["name"]))
func (a *DynamicCustomPrompt) CanRender() bool {
_, err := a.i.Eval(fmt.Sprintf("%s.Render", a.config["name"]))
if err != nil {
return "", err
return false
}
run := v.Interface().(func() (string, error))
return run()
return true
}
func (a *DynamicPrompt) Role() string {
func (a *DynamicCustomPrompt) Render(c *agent.Agent) (types.PromptResult, error) {
v, err := a.i.Eval(fmt.Sprintf("%s.Render", a.config["name"]))
if err != nil {
return types.PromptResult{}, err
}
run := v.Interface().(func() (string, string, error))
content, image, err := run()
if err != nil {
return types.PromptResult{}, err
}
return types.PromptResult{
Content: content,
ImageBase64: image,
}, nil
}
func (a *DynamicCustomPrompt) Role() string {
v, err := a.i.Eval(fmt.Sprintf("%s.Role", a.config["name"]))
if err != nil {
return "system"
+46
View File
@@ -0,0 +1,46 @@
package prompts
import (
"context"
"github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
)
type MemoryLayer interface {
Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error)
}
type MemoryPrompt struct {
config map[string]string
memory MemoryLayer
}
func NewMemoryPrompt(config map[string]string, memory MemoryLayer) *MemoryPrompt {
return &MemoryPrompt{
config: config,
memory: memory,
}
}
func NewMemoryPromptConfigMeta() config.FieldGroup {
return config.FieldGroup{
Name: "memory",
Label: "Memory",
}
}
func (a *MemoryPrompt) Render(c *agent.Agent) (types.PromptResult, error) {
result, err := a.memory.Run(c.Context(), c.SharedState(), types.ActionParams{})
if err != nil {
return types.PromptResult{}, err
}
return types.PromptResult{
Content: result.Result,
}, nil
}
func (a *MemoryPrompt) Role() string {
return "system"
}
+11 -10
View File
@@ -15,8 +15,8 @@ import (
. "github.com/onsi/gomega"
)
var _ = Describe("Agent test", func() {
Context("Creates an agent and it answers", func() {
var _ = Describe("E2E test", func() {
Context("Creates an agent and it answers", Label("E2E"), func() {
BeforeEach(func() {
Eventually(func() error {
// test apiURL is working and available
@@ -24,7 +24,7 @@ var _ = Describe("Agent test", func() {
return err
}, "10m", "10s").ShouldNot(HaveOccurred())
client := localagi.NewClient(localagiURL, "", time.Minute)
client := localagi.NewClient(localagiURL, "", 5*time.Minute)
err := client.DeleteAgent("testagent1")
Expect(err).ToNot(HaveOccurred())
})
@@ -44,7 +44,7 @@ var _ = Describe("Agent test", func() {
})
})
Context("Can do user defined tool calls", Ordered, func() {
Context("Can do user defined tool calls", Ordered, Label("E2E"), func() {
BeforeAll(func() {
Eventually(func() error {
// test apiURL is working and available
@@ -56,13 +56,14 @@ var _ = Describe("Agent test", func() {
err := client.DeleteAgent("testagent2")
Expect(err).ToNot(HaveOccurred())
err = client.CreateAgent(&localagi.AgentConfig{
Name: "testagent2",
Name: "testagent2",
EnableReasoning: true,
})
Expect(err).ToNot(HaveOccurred())
})
It("can create a task", func() {
client := localagi.NewClient(localagiURL, "", 5*time.Minute)
client := localagi.NewClient(localagiURL, "", 10*time.Minute)
req := localagi.RequestBody{
Model: "testagent2",
@@ -170,7 +171,7 @@ var _ = Describe("Agent test", func() {
msg, err := out.ToMessage()
if err == nil && msg.Role == "assistant" {
xlog.Info("Agent returned message", "message", msg)
Expect(len(result.Output)).To(BeNumerically(">", 1))
Expect(len(result.Output)).To(BeNumerically(">", 1), fmt.Sprintf("%+v", result.Output))
continue
}
fnc, err := out.ToFunctionToolCall()
@@ -204,7 +205,7 @@ var _ = Describe("Agent test", func() {
localagi.InputMessage{
Type: "message",
Role: "user",
Content: "Was the appointment created?",
Content: "Was the appointment created? Reply using the ChooseAnswer tool.",
},
},
Tools: []localagi.Tool{
@@ -235,12 +236,12 @@ var _ = Describe("Agent test", func() {
Expect(err).ToNot(HaveOccurred())
Expect(len(result.Output)).To(BeNumerically(">", 0))
fnc, err := result.Output[len(result.Output)-1].ToFunctionToolCall()
Expect(err).ToNot(HaveOccurred())
Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%+v", result))
Expect(fnc.Arguments).To(ContainSubstring("true"))
})
It("can tool call; web search", func() {
client := localagi.NewClient(localagiURL, "", 5*time.Minute)
client := localagi.NewClient(localagiURL, "", 10*time.Minute)
req := localagi.RequestBody{
Model: "testagent2",
+9 -16
View File
@@ -25,14 +25,11 @@ import (
"github.com/mudler/LocalAGI/core/sse"
"github.com/mudler/LocalAGI/core/state"
"github.com/donseba/go-htmx"
fiber "github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html/v2"
)
type (
App struct {
htmx *htmx.HTMX
config *Config
*fiber.App
sharedState *internalTypes.AgentSharedState
@@ -41,16 +38,12 @@ type (
func NewApp(opts ...Option) *App {
config := NewConfig(opts...)
engine := html.NewFileSystem(http.FS(viewsfs), ".html")
// Initialize a new Fiber app
// Pass the engine to the Views
webapp := fiber.New(fiber.Config{
Views: engine,
})
webapp := fiber.New(fiber.Config{})
a := &App{
htmx: htmx.New(),
config: config,
App: webapp,
sharedState: internalTypes.NewAgentSharedState(5 * time.Minute),
@@ -492,7 +485,7 @@ func (a *App) ListActions() func(c *fiber.Ctx) error {
func (a *App) createToolCallResponse(id, agentName string, actionState coreTypes.ActionState, conv []openai.ChatCompletionMessage) types.ResponseBody {
// Create tool call ID
toolCallID := fmt.Sprintf("call_%d", time.Now().UnixNano())
// Get function name and arguments
functionName := actionState.Action.Definition().Name.String()
argumentsJSON, err := json.Marshal(actionState.Params)
@@ -501,7 +494,7 @@ func (a *App) createToolCallResponse(id, agentName string, actionState coreTypes
// Fallback to empty arguments
argumentsJSON = []byte("{}")
}
// Create message object with reasoning
messageObj := types.ResponseMessage{
Type: "message",
@@ -515,7 +508,7 @@ func (a *App) createToolCallResponse(id, agentName string, actionState coreTypes
},
},
}
// Create function tool call object
functionToolCall := types.FunctionToolCall{
Arguments: string(argumentsJSON),
@@ -525,7 +518,7 @@ func (a *App) createToolCallResponse(id, agentName string, actionState coreTypes
ID: fmt.Sprintf("tool_%d", time.Now().UnixNano()),
Status: "completed",
}
// Create response with both message and tool call in output array
return types.ResponseBody{
ID: id,
@@ -612,7 +605,7 @@ func (a *App) Responses(pool *state.AgentPool, tracker *conversations.Conversati
lastAction := res.State[len(res.State)-1]
if coreTypes.IsActionUserDefined(lastAction.Action) {
xlog.Debug("Detected user-defined action, creating tool call response", "action", lastAction.Action.Definition().Name)
// Generate tool call response
response := a.createToolCallResponse(id, agentName, lastAction, conv)
tracker.SetConversation(id, conv) // Save conversation without adding assistant message
@@ -737,13 +730,13 @@ func (a *App) CreateGroup(pool *state.AgentPool) func(c *fiber.Ctx) error {
}
// GetAgentConfigMeta returns the metadata for agent configuration fields
func (a *App) GetAgentConfigMeta() func(c *fiber.Ctx) error {
func (a *App) GetAgentConfigMeta(customDirectory string) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
// Create a new instance of AgentConfigMeta
configMeta := state.NewAgentConfigMeta(
services.ActionsConfigMeta(),
services.ActionsConfigMeta(customDirectory),
services.ConnectorsConfigMeta(),
services.DynamicPromptsConfigMeta(),
services.DynamicPromptsConfigMeta(customDirectory),
services.FiltersConfigMeta(),
)
return c.JSON(configMeta)
-714
View File
@@ -1,714 +0,0 @@
:root {
--primary: #00ff95;
--secondary: #ff00b1;
--tertiary: #5e00ff;
--dark-bg: #111111;
--darker-bg: #0a0a0a;
--medium-bg: #222222;
--light-bg: #333333;
--neon-glow: 0 0 8px rgba(0, 255, 149, 0.7);
--pink-glow: 0 0 8px rgba(255, 0, 177, 0.7);
--purple-glow: 0 0 8px rgba(94, 0, 255, 0.7);
}
/* Glitch effect animation */
@keyframes glitch {
0% { transform: translate(0); }
20% { transform: translate(-2px, 2px); }
40% { transform: translate(-2px, -2px); }
60% { transform: translate(2px, 2px); }
80% { transform: translate(2px, -2px); }
100% { transform: translate(0); }
}
/* Neon pulse animation */
@keyframes neonPulse {
0% { text-shadow: 0 0 7px var(--primary), 0 0 10px var(--primary); }
50% { text-shadow: 0 0 15px var(--primary), 0 0 25px var(--primary); }
100% { text-shadow: 0 0 7px var(--primary), 0 0 10px var(--primary); }
}
/* Scanning line effect */
@keyframes scanline {
0% { transform: translateY(-100%); }
100% { transform: translateY(100%); }
}
body {
font-family: 'Outfit', sans-serif;
background-color: var(--dark-bg);
color: #ffffff;
padding: 20px;
position: relative;
overflow-x: hidden;
background-image:
radial-gradient(circle at 10% 20%, rgba(0, 255, 149, 0.05) 0%, transparent 20%),
radial-gradient(circle at 90% 80%, rgba(255, 0, 177, 0.05) 0%, transparent 20%),
radial-gradient(circle at 50% 50%, rgba(94, 0, 255, 0.05) 0%, transparent 30%),
linear-gradient(180deg, var(--darker-bg) 0%, var(--dark-bg) 100%);
background-attachment: fixed;
}
body::before {
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
transparent,
transparent 2px,
rgba(0, 0, 0, 0.1) 2px,
rgba(0, 0, 0, 0.1) 4px
);
pointer-events: none;
z-index: 1000;
opacity: 0.3;
}
body::after {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, var(--primary), var(--secondary));
opacity: 0.7;
z-index: 1001;
animation: scanline 6s linear infinite;
pointer-events: none;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 700;
}
h1 {
font-family: 'Permanent Marker', cursive;
color: var(--primary);
text-shadow: var(--neon-glow);
margin-bottom: 1rem;
position: relative;
animation: neonPulse 2s infinite;
}
h1:hover {
animation: glitch 0.3s infinite;
}
h2 {
font-size: 1.5rem;
color: var(--secondary);
text-shadow: var(--pink-glow);
margin-bottom: 0.5rem;
}
.section-box {
background-color: rgba(17, 17, 17, 0.85);
border: 1px solid var(--primary);
padding: 25px;
margin-bottom: 20px;
border-radius: 6px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4), 0 0 0 1px var(--primary), inset 0 0 20px rgba(0, 0, 0, 0.3);
position: relative;
overflow: hidden;
}
.section-box::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, var(--primary), var(--secondary), var(--tertiary), var(--primary));
background-size: 200% 100%;
animation: gradientMove 3s linear infinite;
}
@keyframes gradientMove {
0% { background-position: 0% 50%; }
100% { background-position: 100% 50%; }
}
input, button, textarea, select {
width: 100%;
padding: 12px;
margin-top: 8px;
border-radius: 4px;
border: 1px solid var(--medium-bg);
background-color: var(--light-bg);
color: white;
transition: all 0.3s ease;
}
input[type="text"], input[type="file"], textarea {
background-color: var(--light-bg);
border-left: 3px solid var(--primary);
color: white;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--primary);
box-shadow: var(--neon-glow);
}
button {
background: linear-gradient(135deg, var(--tertiary), var(--secondary));
color: white;
cursor: pointer;
border: none;
position: relative;
overflow: hidden;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.3s ease;
}
button::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: all 0.5s;
}
button:hover {
transform: translateY(-3px);
box-shadow: 0 7px 14px rgba(0, 0, 0, 0.3), 0 0 10px rgba(94, 0, 255, 0.5);
}
button:hover::before {
left: 100%;
}
textarea {
height: 200px;
resize: vertical;
}
/* Select styling */
select {
appearance: none;
background-color: var(--light-bg);
border-left: 3px solid var(--tertiary);
color: white;
padding: 12px;
border-radius: 4px;
background-image: url('data:image/svg+xml;utf8,<svg fill="%23ffffff" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/></svg>');
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 12px;
cursor: pointer;
}
select:hover {
border-color: var(--secondary);
box-shadow: 0 0 0 1px var(--secondary);
}
select:focus {
border-color: var(--tertiary);
box-shadow: var(--purple-glow);
}
select {
overflow-y: auto;
}
option {
background-color: var(--medium-bg);
color: white;
padding: 8px 10px;
}
/* Custom Scrollbars */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--medium-bg);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(var(--primary), var(--secondary));
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--tertiary);
}
/* Checkbox styling */
.checkbox-custom {
position: relative;
display: inline-block;
width: 22px;
height: 22px;
margin: 5px;
cursor: pointer;
vertical-align: middle;
}
.checkbox-custom input {
opacity: 0;
width: 0;
height: 0;
}
.checkbox-custom .checkmark {
position: absolute;
top: 0;
left: 0;
height: 22px;
width: 22px;
background-color: var(--light-bg);
border-radius: 4px;
border: 1px solid var(--medium-bg);
transition: all 0.3s ease;
}
.checkbox-custom:hover .checkmark {
border-color: var(--primary);
box-shadow: var(--neon-glow);
}
.checkbox-custom input:checked ~ .checkmark {
background: linear-gradient(135deg, var(--primary), var(--tertiary));
border-color: transparent;
}
.checkbox-custom .checkmark:after {
content: "";
position: absolute;
display: none;
}
.checkbox-custom input:checked ~ .checkmark:after {
display: block;
}
.checkbox-custom .checkmark:after {
left: 8px;
top: 4px;
width: 6px;
height: 12px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
/* Card styling */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.card-link {
text-decoration: none;
display: block;
}
.card {
background: linear-gradient(145deg, rgba(34, 34, 34, 0.9), rgba(17, 17, 17, 0.9));
border: 1px solid rgba(94, 0, 255, 0.2);
border-radius: 8px;
padding: 25px;
margin: 25px auto;
text-align: left;
width: 90%;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
}
.card::before {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, var(--primary), var(--secondary), var(--tertiary));
transform: scaleX(0);
transform-origin: left;
transition: transform 0.4s ease-out;
}
.card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4), 0 0 15px rgba(94, 0, 255, 0.3);
}
.card:hover::before {
transform: scaleX(1);
}
.card h2 {
font-family: 'Outfit', sans-serif;
font-size: 1.5em;
font-weight: 600;
color: var(--primary);
margin-bottom: 0.8em;
position: relative;
display: inline-block;
}
.card a {
color: var(--secondary);
transition: color 0.3s;
text-decoration: none;
position: relative;
}
.card a:hover {
color: var(--primary);
}
.card a::after {
content: "";
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 1px;
background: var(--primary);
transform: scaleX(0);
transform-origin: right;
transition: transform 0.3s ease;
}
.card a:hover::after {
transform: scaleX(1);
transform-origin: left;
}
.card p {
color: #cccccc;
font-size: 1em;
line-height: 1.6;
}
/* Button container */
.button-container {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-bottom: 12px;
}
/* Alert and Toast styling */
.alert {
padding: 12px 15px;
border-radius: 4px;
margin: 15px 0;
display: none;
position: relative;
border-left: 4px solid;
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.alert-success {
background-color: rgba(0, 255, 149, 0.1);
border-color: var(--primary);
color: var(--primary);
}
.alert-error {
background-color: rgba(255, 0, 177, 0.1);
border-color: var(--secondary);
color: var(--secondary);
}
.toast {
position: fixed;
top: 30px;
right: 30px;
max-width: 350px;
padding: 15px 20px;
border-radius: 6px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5);
z-index: 2000;
opacity: 0;
transform: translateX(30px);
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
display: flex;
align-items: center;
}
.toast::before {
content: "";
width: 20px;
height: 20px;
margin-right: 15px;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
.toast-success {
background: linear-gradient(135deg, rgba(0, 255, 149, 0.9), rgba(0, 255, 149, 0.7));
color: #111111;
border-left: 4px solid var(--primary);
}
.toast-success::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23111111'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E");
}
.toast-error {
background: linear-gradient(135deg, rgba(255, 0, 177, 0.9), rgba(255, 0, 177, 0.7));
color: #ffffff;
border-left: 4px solid var(--secondary);
}
.toast-error::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E");
}
.toast-visible {
opacity: 1;
transform: translateX(0);
}
/* Action buttons */
.action-btn {
background: var(--medium-bg);
color: white;
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
font-size: 0.9rem;
display: inline-flex;
align-items: center;
gap: 8px;
}
.action-btn i {
font-size: 1rem;
}
.action-btn:hover {
transform: translateY(-2px);
}
.start-btn {
background: linear-gradient(135deg, var(--primary), rgba(0, 255, 149, 0.7));
color: #111111;
border: none;
}
.start-btn:hover {
box-shadow: 0 0 15px rgba(0, 255, 149, 0.5);
background: var(--primary);
}
.pause-btn {
background: linear-gradient(135deg, var(--tertiary), rgba(94, 0, 255, 0.7));
color: white;
border: none;
}
.pause-btn:hover {
box-shadow: 0 0 15px rgba(94, 0, 255, 0.5);
background: var(--tertiary);
}
/* Badge styling */
.badge {
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge-primary {
background-color: var(--primary);
color: #111111;
}
.badge-secondary {
background-color: var(--secondary);
color: white;
}
.badge-tertiary {
background-color: var(--tertiary);
color: white;
}
/* Data display tables */
.data-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: 20px 0;
border-radius: 6px;
overflow: hidden;
}
.data-table th, .data-table td {
text-align: left;
padding: 12px 15px;
border-bottom: 1px solid var(--medium-bg);
}
.data-table th {
background-color: rgba(94, 0, 255, 0.2);
color: var(--tertiary);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 0.85rem;
}
.data-table tr:last-child td {
border-bottom: none;
}
.data-table tr:nth-child(odd) td {
background-color: rgba(17, 17, 17, 0.6);
}
.data-table tr:nth-child(even) td {
background-color: rgba(34, 34, 34, 0.6);
}
.data-table tr:hover td {
background-color: rgba(94, 0, 255, 0.1);
}
/* Terminal-style code display */
.code-terminal {
background-color: #0a0a0a;
border-radius: 6px;
padding: 15px;
font-family: 'Courier New', monospace;
color: #00ff95;
margin: 20px 0;
position: relative;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.4);
overflow: hidden;
}
.code-terminal::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 25px;
background: #222;
display: flex;
align-items: center;
padding: 0 10px;
}
.code-terminal::after {
content: "• • •";
position: absolute;
top: 0;
left: 12px;
height: 25px;
display: flex;
align-items: center;
color: #666;
font-size: 20px;
letter-spacing: -2px;
}
.code-terminal pre {
margin-top: 25px;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
}
.code-terminal .prompt {
color: var(--secondary);
user-select: none;
}
/* User info badge */
.user-info {
display: flex;
align-items: center;
background: linear-gradient(135deg, rgba(17, 17, 17, 0.8), rgba(34, 34, 34, 0.8));
border: 1px solid var(--tertiary);
border-radius: 30px;
padding: 6px 15px;
margin: 10px 0;
font-size: 0.9rem;
box-shadow: var(--purple-glow);
}
.user-info::before {
content: "";
width: 10px;
height: 10px;
background-color: var(--primary);
border-radius: 50%;
margin-right: 10px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(0, 255, 149, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(0, 255, 149, 0); }
100% { box-shadow: 0 0 0 0 rgba(0, 255, 149, 0); }
}
.timestamp {
margin-left: auto;
font-family: 'Courier New', monospace;
color: var(--secondary);
}
/* Responsive design adjustments */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.card {
width: 100%;
padding: 15px;
}
.section-box {
padding: 15px;
}
.button-container {
flex-direction: column;
}
.toast {
top: 10px;
right: 10px;
left: 10px;
max-width: none;
}
}
-254
View File
@@ -1,254 +0,0 @@
/* Agent Form Wizard Styles */
.agent-form-container {
display: flex;
gap: 2rem;
margin-bottom: 2rem;
}
/* Wizard Sidebar */
.wizard-sidebar {
width: 250px;
background: var(--surface);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1.5rem 0;
flex-shrink: 0;
}
.wizard-nav {
list-style: none;
padding: 0;
margin: 0;
}
.wizard-nav-item {
padding: 12px 20px;
cursor: pointer;
transition: all 0.2s ease;
border-left: 4px solid transparent;
display: flex;
align-items: center;
}
.wizard-nav-item i {
margin-right: 10px;
width: 20px;
text-align: center;
}
.wizard-nav-item:hover {
background: rgba(var(--primary-rgb), 0.1);
}
.wizard-nav-item.active {
background: rgba(var(--primary-rgb), 0.15);
border-left-color: var(--primary);
color: var(--primary);
font-weight: 600;
}
/* Form Content Area */
.form-content-area {
flex: 1;
padding: 1.5rem;
background: var(--surface);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 1.5rem;
margin-bottom: 1.5rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(var(--border-rgb), 0.5);
color: var(--text);
}
.form-section {
display: none;
}
.form-section.active {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Improved input styles */
.mb-4 {
margin-bottom: 1.5rem;
}
.form-section label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text);
}
.form-section input[type="text"],
.form-section input[type="number"],
.form-section textarea,
.form-section select {
width: 100%;
padding: 10px 12px;
border-radius: 6px;
border: 1px solid rgba(var(--border-rgb), 0.8);
background-color: var(--input-bg);
color: var(--text);
font-size: 16px;
transition: border-color 0.2s ease;
}
.form-section textarea {
min-height: 120px;
resize: vertical;
}
.form-section input[type="text"]:focus,
.form-section input[type="number"]:focus,
.form-section textarea:focus,
.form-section select:focus {
border-color: var(--primary);
outline: none;
box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.2);
}
/* Button Styles */
.button-container {
margin: 1.5rem 0;
}
.action-btn {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
border: none;
padding: 10px 16px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
}
.action-btn i {
margin-right: 6px;
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.action-btn:active {
transform: translateY(0);
}
/* Navigation controls - improved layout */
.wizard-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid rgba(var(--border-rgb), 0.5);
}
.wizard-controls-left {
flex: 1;
text-align: left;
}
.wizard-controls-center {
flex: 2;
text-align: center;
}
.wizard-controls-right {
flex: 1;
text-align: right;
}
.nav-btn {
background: var(--surface);
color: var(--text);
border: 1px solid rgba(var(--border-rgb), 0.8);
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
}
.nav-btn i {
margin-right: 6px;
}
.nav-btn:last-child i {
margin-right: 0;
margin-left: 6px;
}
.nav-btn:hover {
background: rgba(var(--primary-rgb), 0.1);
}
.progress-indicator {
display: inline-block;
font-size: 0.9rem;
color: var(--text-muted);
font-weight: 500;
margin-top: 0.5rem;
}
.progress-dots {
display: flex;
justify-content: center;
margin-bottom: 8px;
gap: 6px;
}
.progress-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: rgba(var(--border-rgb), 0.4);
transition: all 0.2s ease;
}
.progress-dot.active {
background-color: var(--primary);
transform: scale(1.2);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.wizard-controls {
flex-direction: column;
gap: 1rem;
}
.wizard-controls-left,
.wizard-controls-center,
.wizard-controls-right {
width: 100%;
text-align: center;
}
.progress-dots {
margin: 12px 0;
}
}
-564
View File
@@ -1,564 +0,0 @@
// Common utility functions for agent forms
const AgentFormUtils = {
// Add dynamic component based on template
addDynamicComponent: function(sectionId, templateFunction, dataItems) {
const section = document.getElementById(sectionId);
const newIndex = section.getElementsByClassName(dataItems.className).length;
// Generate HTML from template function
const newHtml = templateFunction(newIndex, dataItems);
// Add to DOM
section.insertAdjacentHTML('beforeend', newHtml);
},
// Process form data into JSON structure
processFormData: function(formData) {
const jsonData = {};
// Process basic form fields
for (const [key, value] of formData.entries()) {
// Skip the array fields as they'll be processed separately
if (!key.includes('[') && !key.includes('].')) {
// Handle checkboxes
if (value === 'on') {
jsonData[key] = true;
}
// Handle numeric fields - specifically kb_results
else if (key === 'kb_results') {
// Convert to integer or default to 3 if empty
jsonData[key] = value ? parseInt(value, 10) : 3;
// Check if the parse was successful
if (isNaN(jsonData[key])) {
showToast('Knowledge Base Results must be a number', 'error');
return null; // Indicate validation error
}
}
// Handle other numeric fields if needed
else if (key === 'periodic_runs' && value) {
// Try to parse as number if it looks like one
const numValue = parseInt(value, 10);
if (!isNaN(numValue) && String(numValue) === value) {
jsonData[key] = numValue;
} else {
jsonData[key] = value;
}
}
else {
jsonData[key] = value;
}
}
}
return jsonData;
},
// Process connectors from form
processConnectors: function(button) {
const connectors = [];
const connectorElements = document.querySelectorAll('.connector');
for (let i = 0; i < connectorElements.length; i++) {
const typeSelect = document.getElementById(`connectorType${i}`);
if (!typeSelect) {
showToast(`Error: Could not find connector type select for index ${i}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
const type = typeSelect.value;
if (!type) {
showToast(`Please select a connector type for connector ${i+1}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
// Get all config fields for this connector
const connector = {
type: type,
config: {}
};
// Find all config inputs for this connector
const configInputs = document.querySelectorAll(`[name^="connectors[${i}][config]"]`);
// Check if we have a JSON textarea (fallback template)
const jsonTextarea = document.getElementById(`connectorConfig${i}`);
if (jsonTextarea && jsonTextarea.value) {
try {
// If it's a JSON textarea, parse it and use the result
const jsonConfig = JSON.parse(jsonTextarea.value);
// Convert the parsed JSON back to a string for the backend
connector.config = JSON.stringify(jsonConfig);
} catch (e) {
// If it's not valid JSON, use it as is
connector.config = jsonTextarea.value;
}
} else {
// Process individual form fields
configInputs.forEach(input => {
// Extract the key from the name attribute
// Format: connectors[0][config][key]
const keyMatch = input.name.match(/\[config\]\[([^\]]+)\]/);
if (keyMatch && keyMatch[1]) {
const key = keyMatch[1];
// For checkboxes, set true/false based on checked state
if (input.type === 'checkbox') {
connector.config[key] = input.checked ? 'true' : 'false';
} else {
connector.config[key] = input.value;
}
}
});
// Convert the config object to a JSON string for the backend
connector.config = JSON.stringify(connector.config);
}
connectors.push(connector);
}
return connectors;
},
// Process MCP servers from form
processMCPServers: function() {
const mcpServers = [];
const mcpElements = document.querySelectorAll('.mcp_server');
for (let i = 0; i < mcpElements.length; i++) {
const urlInput = document.getElementById(`mcpURL${i}`);
const tokenInput = document.getElementById(`mcpToken${i}`);
if (urlInput && urlInput.value) {
const server = {
url: urlInput.value
};
// Add token if present
if (tokenInput && tokenInput.value) {
server.token = tokenInput.value;
}
mcpServers.push(server);
}
}
return mcpServers;
},
// Process actions from form
processActions: function(button) {
const actions = [];
const actionElements = document.querySelectorAll('.action');
for (let i = 0; i < actionElements.length; i++) {
const nameSelect = document.getElementById(`actionsName${i}`);
const configTextarea = document.getElementById(`actionsConfig${i}`);
if (!nameSelect) {
showToast(`Error: Could not find action name select for index ${i}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
const name = nameSelect.value;
if (!name) {
showToast(`Please select an action type for action ${i+1}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
let config = {};
if (configTextarea && configTextarea.value) {
try {
config = JSON.parse(configTextarea.value);
} catch (e) {
showToast(`Invalid JSON in action ${i+1} config: ${e.message}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
}
actions.push({
name: name,
config: JSON.stringify(config) // Convert to JSON string for backend
});
}
return actions;
},
// Process prompt blocks from form
processPromptBlocks: function(button) {
const promptBlocks = [];
const promptElements = document.querySelectorAll('.prompt_block');
for (let i = 0; i < promptElements.length; i++) {
const nameSelect = document.getElementById(`promptName${i}`);
const configTextarea = document.getElementById(`promptConfig${i}`);
if (!nameSelect) {
showToast(`Error: Could not find prompt block name select for index ${i}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
const name = nameSelect.value;
if (!name) {
showToast(`Please select a prompt block type for block ${i+1}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
let config = {};
if (configTextarea && configTextarea.value) {
try {
config = JSON.parse(configTextarea.value);
} catch (e) {
showToast(`Invalid JSON in prompt block ${i+1} config: ${e.message}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
}
promptBlocks.push({
name: name,
config: JSON.stringify(config) // Convert to JSON string for backend
});
}
return promptBlocks;
},
// Helper function to format config values (for edit form)
formatConfigValue: function(configElement, configValue) {
if (!configElement) return;
// If configValue is an object, stringify it
if (typeof configValue === 'object' && configValue !== null) {
try {
configElement.value = JSON.stringify(configValue, null, 2);
} catch (e) {
console.error('Error stringifying config value:', e);
configElement.value = '{}';
}
}
// If it's a string that looks like JSON, try to parse and pretty print it
else if (typeof configValue === 'string' && (configValue.startsWith('{') || configValue.startsWith('['))) {
try {
const parsed = JSON.parse(configValue);
configElement.value = JSON.stringify(parsed, null, 2);
} catch (e) {
// If it's not valid JSON, just use the string as is
configElement.value = configValue;
}
}
// Otherwise, just use the value as is
else {
configElement.value = configValue || '';
}
},
// Helper function to set select value (with fallback if option doesn't exist)
setSelectValue: function(selectElement, value) {
if (!selectElement) return;
// Check if the option exists
let optionExists = false;
for (let i = 0; i < selectElement.options.length; i++) {
if (selectElement.options[i].value === value) {
optionExists = true;
break;
}
}
// Set the value if the option exists
if (optionExists) {
selectElement.value = value;
} else if (selectElement.options.length > 0) {
// Otherwise, select the first option
selectElement.selectedIndex = 0;
}
},
// Render connector form based on type
renderConnectorForm: function(index, type, config = {}) {
const formContainer = document.getElementById(`connectorFormContainer${index}`);
if (!formContainer) return;
// Clear existing form
formContainer.innerHTML = '';
// Debug log to see what's happening
console.log(`Rendering connector form for type: ${type}`);
console.log(`Config for connector:`, config);
console.log(`Available templates:`, ConnectorTemplates ? Object.keys(ConnectorTemplates) : 'None');
// Ensure config is an object
let configObj = config;
if (typeof config === 'string') {
try {
configObj = JSON.parse(config);
} catch (e) {
console.error('Error parsing connector config string:', e);
configObj = {};
}
}
// If we have a template for this connector type in the global ConnectorTemplates object
if (ConnectorTemplates && type && ConnectorTemplates[type]) {
console.log(`Found template for ${type}`);
// Get the template result which contains HTML and setValues function
const templateResult = ConnectorTemplates[type](configObj, index);
// Set the HTML content
formContainer.innerHTML = templateResult.html;
// Call the setValues function to set input values safely
if (typeof templateResult.setValues === 'function') {
setTimeout(templateResult.setValues, 0);
}
} else {
console.log(`No template found for ${type}, using fallback`);
// Use the fallback template
if (ConnectorTemplates && ConnectorTemplates.fallback) {
const fallbackResult = ConnectorTemplates.fallback(configObj, index);
formContainer.innerHTML = fallbackResult.html;
if (typeof fallbackResult.setValues === 'function') {
setTimeout(fallbackResult.setValues, 0);
}
} else {
// Fallback to generic JSON textarea if no fallback template
formContainer.innerHTML = `
<div class="form-group">
<label for="connectorConfig${index}">Connector Config (JSON)</label>
<textarea id="connectorConfig${index}"
name="connectors[${index}][config]"
class="form-control"
placeholder='{"key":"value"}'></textarea>
</div>
`;
// Set the value safely after DOM is created
setTimeout(function() {
const configTextarea = document.getElementById(`connectorConfig${index}`);
if (configTextarea) {
if (typeof configObj === 'object' && configObj !== null) {
configTextarea.value = JSON.stringify(configObj, null, 2);
} else if (typeof config === 'string') {
configTextarea.value = config;
}
}
}, 0);
}
}
}
};
// HTML Templates for dynamic elements
const AgentFormTemplates = {
// Connector template
connectorTemplate: function(index, data) {
return `
<div class="connector mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>Connector ${index + 1}</h2>
<div class="mb-4">
<label for="connectorType${index}">Connector Type</label>
<select name="connectors[${index}][type]"
id="connectorType${index}"
class="form-control"
onchange="AgentFormUtils.renderConnectorForm(${index}, this.value)">
<option value="">Select Connector Type</option>
${data.options}
</select>
</div>
<div id="connectorFormContainer${index}">
<!-- Connector form will be dynamically inserted here -->
<div class="form-group">
<div class="placeholder-text">Select a connector type to configure</div>
</div>
</div>
<button type="button" class="remove-btn" onclick="this.closest('.connector').remove()">
<i class="fas fa-trash"></i> Remove Connector
</button>
</div>
`;
},
// MCP Server template
mcpServerTemplate: function(index, data) {
return `
<div class="mcp_server mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>MCP Server ${index + 1}</h2>
<div class="mb-4">
<label for="mcpURL${index}">Server URL</label>
<input type="text" id="mcpURL${index}" name="mcp_servers[${index}][url]" placeholder="https://example.com">
</div>
<div class="mb-4">
<label for="mcpToken${index}">API Token (Optional)</label>
<input type="text" id="mcpToken${index}" name="mcp_servers[${index}][token]" placeholder="API token">
</div>
<button type="button" class="remove-btn" onclick="this.closest('.mcp_server').remove()">
<i class="fas fa-trash"></i> Remove Server
</button>
</div>
`;
},
// Action template
actionTemplate: function(index, data) {
return `
<div class="action mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>Action ${index + 1}</h2>
<div class="mb-4">
<label for="actionsName${index}">Action Type</label>
<select name="actions[${index}][name]" id="actionsName${index}">
${data.options}
</select>
</div>
<div class="mb-4">
<label for="actionsConfig${index}">Action Config (JSON)</label>
<textarea id="actionsConfig${index}" name="actions[${index}][config]" placeholder='{"key":"value"}'>{}</textarea>
</div>
<button type="button" class="remove-btn" onclick="this.closest('.action').remove()">
<i class="fas fa-trash"></i> Remove Action
</button>
</div>
`;
},
// Prompt Block template
promptBlockTemplate: function(index, data) {
return `
<div class="prompt_block mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>Prompt Block ${index + 1}</h2>
<div class="mb-4">
<label for="promptName${index}">Prompt Block Type</label>
<select name="promptblocks[${index}][name]" id="promptName${index}">
${data.options}
</select>
</div>
<div class="mb-4">
<label for="promptConfig${index}">Prompt Block Config (JSON)</label>
<textarea id="promptConfig${index}" name="promptblocks[${index}][config]" placeholder='{"key":"value"}'>{}</textarea>
</div>
<button type="button" class="remove-btn" onclick="this.closest('.prompt_block').remove()">
<i class="fas fa-trash"></i> Remove Prompt Block
</button>
</div>
`;
}
};
// Initialize form event listeners
function initAgentFormCommon(options = {}) {
// Add connector button
const addConnectorButton = document.getElementById('addConnectorButton');
if (addConnectorButton) {
addConnectorButton.addEventListener('click', function() {
// Create options string
let optionsHtml = '';
if (options.connectors) {
optionsHtml = options.connectors;
}
// Add new connector form
AgentFormUtils.addDynamicComponent('connectorsSection', AgentFormTemplates.connectorTemplate, {
className: 'connector',
options: optionsHtml
});
});
}
// Add MCP server button
const addMCPButton = document.getElementById('addMCPButton');
if (addMCPButton) {
addMCPButton.addEventListener('click', function() {
// Add new MCP server form
AgentFormUtils.addDynamicComponent('mcpSection', AgentFormTemplates.mcpServerTemplate, {
className: 'mcp_server'
});
});
}
// Add action button
const actionButton = document.getElementById('action_button');
if (actionButton) {
actionButton.addEventListener('click', function() {
// Create options string
let optionsHtml = '';
if (options.actions) {
optionsHtml = options.actions;
}
// Add new action form
AgentFormUtils.addDynamicComponent('action_box', AgentFormTemplates.actionTemplate, {
className: 'action',
options: optionsHtml
});
});
}
// Add prompt block button
const dynamicButton = document.getElementById('dynamic_button');
if (dynamicButton) {
dynamicButton.addEventListener('click', function() {
// Create options string
let optionsHtml = '';
if (options.promptBlocks) {
optionsHtml = options.promptBlocks;
}
// Add new prompt block form
AgentFormUtils.addDynamicComponent('dynamic_box', AgentFormTemplates.promptBlockTemplate, {
className: 'prompt_block',
options: optionsHtml
});
});
}
}
// Simple toast notification function
function showToast(message, type) {
// Check if toast container exists, if not create it
let toast = document.getElementById('toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'toast';
toast.className = 'toast';
const toastMessage = document.createElement('div');
toastMessage.id = 'toast-message';
toast.appendChild(toastMessage);
document.body.appendChild(toast);
}
const toastMessage = document.getElementById('toast-message');
// Set message
toastMessage.textContent = message;
// Set type class
toast.className = 'toast';
toast.classList.add(`toast-${type}`);
// Show toast
toast.classList.add('show');
// Hide after 3 seconds
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
-44
View File
@@ -1,44 +0,0 @@
// Function to show toast notifications with enhanced animation
function showToast(message, type) {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toast-message');
// Set message
toastMessage.textContent = message;
// Set toast type (success/error)
toast.className = 'toast';
toast.classList.add(type === 'success' ? 'toast-success' : 'toast-error');
// Show toast with enhanced animation
setTimeout(() => {
toast.classList.add('toast-visible');
}, 100);
// Hide toast after 3 seconds with animation
setTimeout(() => {
toast.classList.remove('toast-visible');
// Clean up after animation completes
setTimeout(() => {
toast.className = 'toast';
}, 400);
}, 3000);
}
// Function to create the glitch effect on headings
document.addEventListener('DOMContentLoaded', function() {
const headings = document.querySelectorAll('h1');
headings.forEach(heading => {
heading.addEventListener('mouseover', function() {
this.style.animation = 'glitch 0.3s infinite';
});
heading.addEventListener('mouseout', function() {
this.style.animation = 'neonPulse 2s infinite';
});
});
});
-475
View File
@@ -1,475 +0,0 @@
/**
* Connector Templates
*
* This file contains templates for all connector types supported by LocalAGI.
* Each template is a function that returns an HTML string for the connector's form.
*
* Note: We don't need to escape HTML in the value attributes because browsers
* handle these values safely when setting them via DOM properties after rendering.
*/
/**
* Connector Templates
* Each function takes a config object and returns an HTML string
*/
const ConnectorTemplates = {
/**
* Telegram Connector Template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
telegram: function(config = {}, index) {
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="telegramToken${index}">Telegram Bot Token</label>
<input type="text"
id="telegramToken${index}"
name="connectors[${index}][config][token]"
class="form-control"
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11">
<small class="form-text text-muted">Get this from @BotFather on Telegram</small>
</div>
`;
// Function to set values after HTML is added to DOM to avoid XSS
const setValues = function() {
const input = document.getElementById(`telegramToken${index}`);
if (input) input.value = config.token || '';
};
return { html, setValues };
},
/**
* Slack Connector Template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
slack: function(config = {}, index) {
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="slackAppToken${index}">Slack App Token</label>
<input type="text"
id="slackAppToken${index}"
name="connectors[${index}][config][appToken]"
class="form-control"
placeholder="xapp-...">
<small class="form-text text-muted">App-level token starting with xapp-</small>
</div>
<div class="form-group">
<label for="slackBotToken${index}">Slack Bot Token</label>
<input type="text"
id="slackBotToken${index}"
name="connectors[${index}][config][botToken]"
class="form-control"
placeholder="xoxb-...">
<small class="form-text text-muted">Bot token starting with xoxb-</small>
</div>
<div class="form-group">
<label for="slackChannelID${index}">Slack Channel ID</label>
<input type="text"
id="slackChannelID${index}"
name="connectors[${index}][config][channelID]"
class="form-control"
placeholder="C012345678">
<small class="form-text text-muted">Channel ID where the bot will operate</small>
</div>
<div class="form-group form-check">
<input type="checkbox"
class="form-check-input"
id="slackAlwaysReply${index}"
name="connectors[${index}][config][alwaysReply]">
<label class="form-check-label" for="slackAlwaysReply${index}">Always Reply</label>
<small class="form-text text-muted">If checked, the bot will reply to all messages in the channel</small>
</div>
`;
// Function to set values after HTML is added to DOM to avoid XSS
const setValues = function() {
const appTokenInput = document.getElementById(`slackAppToken${index}`);
const botTokenInput = document.getElementById(`slackBotToken${index}`);
const channelIDInput = document.getElementById(`slackChannelID${index}`);
const alwaysReplyInput = document.getElementById(`slackAlwaysReply${index}`);
if (appTokenInput) appTokenInput.value = config.appToken || '';
if (botTokenInput) botTokenInput.value = config.botToken || '';
if (channelIDInput) channelIDInput.value = config.channelID || '';
if (alwaysReplyInput) alwaysReplyInput.checked = config.alwaysReply === 'true';
};
return { html, setValues };
},
/**
* Discord Connector Template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
discord: function(config = {}, index) {
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="discordToken${index}">Discord Bot Token</label>
<input type="text"
id="discordToken${index}"
name="connectors[${index}][config][token]"
class="form-control"
placeholder="Bot token from Discord Developer Portal">
</div>
<div class="form-group">
<label for="discordChannelID${index}">Default Channel ID</label>
<input type="text"
id="discordChannelID${index}"
name="connectors[${index}][config][defaultChannel]"
class="form-control"
placeholder="Channel ID where the bot will operate">
</div>
`;
// Function to set values after HTML is added to DOM
const setValues = function() {
const tokenInput = document.getElementById(`discordToken${index}`);
const channelIDInput = document.getElementById(`discordChannelID${index}`);
if (tokenInput) tokenInput.value = config.token || '';
if (channelIDInput) channelIDInput.value = config.defaultChannel || '';
};
return { html, setValues };
},
/**
* GitHub Issues Connector Template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
'github-issues': function(config = {}, index) {
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="githubIssuesToken${index}">GitHub Personal Access Token</label>
<input type="text"
id="githubIssuesToken${index}"
name="connectors[${index}][config][token]"
class="form-control"
placeholder="ghp_...">
<small class="form-text text-muted">Needs repo and read:org permissions</small>
</div>
<div class="form-group">
<label for="githubIssuesOwner${index}">Repository Owner</label>
<input type="text"
id="githubIssuesOwner${index}"
name="connectors[${index}][config][owner]"
class="form-control"
placeholder="username or organization">
</div>
<div class="form-group">
<label for="githubIssuesRepo${index}">Repository Name</label>
<input type="text"
id="githubIssuesRepo${index}"
name="connectors[${index}][config][repository]"
class="form-control"
placeholder="repository-name">
</div>
<div class="form-group form-check">
<input type="checkbox"
class="form-check-input"
id="githubIssuesReplyIfNoReplies${index}"
name="connectors[${index}][config][replyIfNoReplies]"
value="true">
<label class="form-check-label" for="githubIssuesReplyIfNoReplies${index}">Reply to issues with no replies</label>
<small class="form-text text-muted">If checked, the bot will reply to issues that have no replies yet</small>
</div>
<div class="form-group">
<label for="githubIssuesPollInterval${index}">Poll Interval (seconds)</label>
<input type="number"
id="githubIssuesPollInterval${index}"
name="connectors[${index}][config][pollInterval]"
class="form-control"
placeholder="60">
<small class="form-text text-muted">How often to check for new issues (in seconds)</small>
</div>
`;
// Function to set values after HTML is added to DOM to avoid XSS
const setValues = function() {
const tokenInput = document.getElementById(`githubIssuesToken${index}`);
const ownerInput = document.getElementById(`githubIssuesOwner${index}`);
const repoInput = document.getElementById(`githubIssuesRepo${index}`);
const replyIfNoRepliesInput = document.getElementById(`githubIssuesReplyIfNoReplies${index}`);
const pollIntervalInput = document.getElementById(`githubIssuesPollInterval${index}`);
if (tokenInput) tokenInput.value = config.token || '';
if (ownerInput) ownerInput.value = config.owner || '';
if (repoInput) repoInput.value = config.repository || '';
if (replyIfNoRepliesInput) replyIfNoRepliesInput.checked = config.replyIfNoReplies === 'true';
if (pollIntervalInput) pollIntervalInput.value = config.pollInterval || '60';
};
return { html, setValues };
},
/**
* GitHub PRs Connector Template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
'github-prs': function(config = {}, index) {
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="githubPRsToken${index}">GitHub Token</label>
<input type="text"
id="githubPRsToken${index}"
name="connectors[${index}][config][token]"
class="form-control"
placeholder="ghp_...">
<small class="form-text text-muted">Personal Access Token with repo permissions</small>
</div>
<div class="form-group">
<label for="githubPRsOwner${index}">Repository Owner</label>
<input type="text"
id="githubPRsOwner${index}"
name="connectors[${index}][config][owner]"
class="form-control"
placeholder="username or organization">
</div>
<div class="form-group">
<label for="githubPRsRepo${index}">Repository Name</label>
<input type="text"
id="githubPRsRepo${index}"
name="connectors[${index}][config][repository]"
class="form-control"
placeholder="repository-name">
</div>
<div class="form-group form-check">
<input type="checkbox"
class="form-check-input"
id="githubPRsReplyIfNoReplies${index}"
name="connectors[${index}][config][replyIfNoReplies]"
value="true">
<label class="form-check-label" for="githubPRsReplyIfNoReplies${index}">Reply to PRs with no replies</label>
<small class="form-text text-muted">If checked, the bot will reply to pull requests that have no replies yet</small>
</div>
<div class="form-group">
<label for="githubPRsPollInterval${index}">Poll Interval (seconds)</label>
<input type="number"
id="githubPRsPollInterval${index}"
name="connectors[${index}][config][pollInterval]"
class="form-control"
placeholder="60">
<small class="form-text text-muted">How often to check for new pull requests (in seconds)</small>
</div>
`;
// Function to set values after HTML is added to DOM to avoid XSS
const setValues = function() {
const tokenInput = document.getElementById(`githubPRsToken${index}`);
const ownerInput = document.getElementById(`githubPRsOwner${index}`);
const repoInput = document.getElementById(`githubPRsRepo${index}`);
const replyIfNoRepliesInput = document.getElementById(`githubPRsReplyIfNoReplies${index}`);
const pollIntervalInput = document.getElementById(`githubPRsPollInterval${index}`);
if (tokenInput) tokenInput.value = config.token || '';
if (ownerInput) ownerInput.value = config.owner || '';
if (repoInput) repoInput.value = config.repository || '';
if (replyIfNoRepliesInput) replyIfNoRepliesInput.checked = config.replyIfNoReplies === 'true';
if (pollIntervalInput) pollIntervalInput.value = config.pollInterval || '60';
};
return { html, setValues };
},
/**
* IRC Connector Template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
irc: function(config = {}, index) {
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="ircServer${index}">IRC Server</label>
<input type="text"
id="ircServer${index}"
name="connectors[${index}][config][server]"
class="form-control"
placeholder="irc.libera.chat">
</div>
<div class="form-group">
<label for="ircPort${index}">Port</label>
<input type="text"
id="ircPort${index}"
name="connectors[${index}][config][port]"
class="form-control"
placeholder="6667">
</div>
<div class="form-group">
<label for="ircChannel${index}">Channel</label>
<input type="text"
id="ircChannel${index}"
name="connectors[${index}][config][channel]"
class="form-control"
placeholder="#channel">
</div>
<div class="form-group">
<label for="ircNick${index}">Nickname</label>
<input type="text"
id="ircNick${index}"
name="connectors[${index}][config][nickname]"
class="form-control"
placeholder="MyBot">
</div>
<div class="form-group form-check">
<input type="checkbox"
class="form-check-input"
id="ircAlwaysReply${index}"
name="connectors[${index}][config][alwaysReply]"
value="true">
<label class="form-check-label" for="ircAlwaysReply${index}">Always reply to messages</label>
<small class="form-text text-muted">If checked, the bot will always reply to messages, even if they are not directed at it</small>
</div>
`;
// Function to set values after HTML is added to DOM
const setValues = function() {
const serverInput = document.getElementById(`ircServer${index}`);
const portInput = document.getElementById(`ircPort${index}`);
const channelInput = document.getElementById(`ircChannel${index}`);
const nickInput = document.getElementById(`ircNick${index}`);
const alwaysReplyInput = document.getElementById(`ircAlwaysReply${index}`);
if (serverInput) serverInput.value = config.server || '';
if (portInput) portInput.value = config.port || '6667';
if (channelInput) channelInput.value = config.channel || '';
if (nickInput) nickInput.value = config.nickname || '';
if (alwaysReplyInput) alwaysReplyInput.checked = config.alwaysReply === 'true';
};
return { html, setValues };
},
/**
* Twitter Connector Template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
twitter: function(config = {}, index) {
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="twitterToken${index}">Twitter API Token</label>
<input type="text"
id="twitterToken${index}"
name="connectors[${index}][config][token]"
class="form-control"
placeholder="Your Twitter API token">
</div>
<div class="form-group">
<label for="twitterBotUsername${index}">Bot Username</label>
<input type="text"
id="twitterBotUsername${index}"
name="connectors[${index}][config][botUsername]"
class="form-control"
placeholder="@YourBotUsername">
<small class="form-text text-muted">Username of your Twitter bot (with or without @)</small>
</div>
<div class="form-group form-check">
<input type="checkbox"
class="form-check-input"
id="twitterNoCharLimit${index}"
name="connectors[${index}][config][noCharacterLimit]"
value="true">
<label class="form-check-label" for="twitterNoCharLimit${index}">Disable character limit</label>
<small class="form-text text-muted">If checked, the bot will not enforce Twitter's character limit</small>
</div>
`;
// Function to set values after HTML is added to DOM
const setValues = function() {
const tokenInput = document.getElementById(`twitterToken${index}`);
const botUsernameInput = document.getElementById(`twitterBotUsername${index}`);
const noCharLimitInput = document.getElementById(`twitterNoCharLimit${index}`);
if (tokenInput) tokenInput.value = config.token || '';
if (botUsernameInput) botUsernameInput.value = config.botUsername || '';
if (noCharLimitInput) noCharLimitInput.checked = config.noCharacterLimit === 'true';
};
return { html, setValues };
},
/**
* Fallback template for any connector without a specific template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
fallback: function(config = {}, index) {
// Convert config to a pretty-printed JSON string
let configStr = '{}';
try {
if (typeof config === 'string') {
// If it's already a string, try to parse it first to pretty-print
configStr = JSON.stringify(JSON.parse(config), null, 2);
} else if (typeof config === 'object' && config !== null) {
configStr = JSON.stringify(config, null, 2);
}
} catch (e) {
console.error('Error formatting config:', e);
// If it's a string but not valid JSON, just use it as is
if (typeof config === 'string') {
configStr = config;
}
}
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="connectorConfig${index}">Connector Configuration (JSON)</label>
<textarea id="connectorConfig${index}"
name="connectors[${index}][config]"
class="form-control"
rows="10"
placeholder='{"key":"value"}'>${escapeHTML(configStr)}</textarea>
<small class="form-text text-muted">Enter the connector configuration as a JSON object</small>
</div>
`;
// Function to set values after HTML is added to DOM
const setValues = function() {
const configInput = document.getElementById(`connectorConfig${index}`);
if (configInput) configInput.value = configStr;
};
return { html, setValues };
}
};
-139
View File
@@ -1,139 +0,0 @@
/**
* Agent Form Wizard - Navigation and UI functionality
*/
document.addEventListener('DOMContentLoaded', function() {
// Check if the wizard exists on the page
const wizardSidebar = document.querySelector('.wizard-sidebar');
if (!wizardSidebar) return;
// Get all sections and nav items
const navItems = document.querySelectorAll('.wizard-nav-item');
const sections = document.querySelectorAll('.form-section');
const prevButton = document.getElementById('prevSection');
const nextButton = document.getElementById('nextSection');
const currentStepLabelEl = document.getElementById('currentStepLabel');
const progressDotsContainer = document.getElementById('progressDots');
// Create progress dots
const totalSteps = sections.length;
// Create dots for each section
if (progressDotsContainer) {
for (let i = 0; i < totalSteps; i++) {
const dot = document.createElement('div');
dot.className = 'progress-dot';
dot.setAttribute('data-index', i);
dot.addEventListener('click', () => setActiveSection(i));
progressDotsContainer.appendChild(dot);
}
}
// Get all progress dots
const progressDots = document.querySelectorAll('.progress-dot');
// Track current active section
let currentSectionIndex = 0;
// Initialize
updateNavigation();
// Add click events to nav items
navItems.forEach((item, index) => {
item.addEventListener('click', () => {
setActiveSection(index);
});
});
// Add click events to prev/next buttons
if (prevButton) {
prevButton.addEventListener('click', () => {
if (currentSectionIndex > 0) {
setActiveSection(currentSectionIndex - 1);
}
});
}
if (nextButton) {
nextButton.addEventListener('click', () => {
if (currentSectionIndex < sections.length - 1) {
setActiveSection(currentSectionIndex + 1);
}
});
}
/**
* Set the active section and update navigation
*/
function setActiveSection(index) {
// Remove active class from all sections and nav items
sections.forEach(section => section.classList.remove('active'));
navItems.forEach(item => item.classList.remove('active'));
progressDots.forEach(dot => dot.classList.remove('active'));
// Add active class to current section, nav item, and dot
sections[index].classList.add('active');
navItems[index].classList.add('active');
if (progressDots[index]) {
progressDots[index].classList.add('active');
}
// Update current section index
currentSectionIndex = index;
// Update navigation state
updateNavigation();
// Scroll to top of section
sections[index].scrollIntoView({behavior: 'smooth', block: 'start'});
}
/**
* Update navigation buttons and progress
*/
function updateNavigation() {
// Update section label
if (currentStepLabelEl && navItems[currentSectionIndex]) {
// Extract text content without the icon
const navText = navItems[currentSectionIndex].textContent.trim();
currentStepLabelEl.textContent = navText;
}
// Update prev/next buttons
if (prevButton) {
prevButton.disabled = currentSectionIndex === 0;
prevButton.style.opacity = currentSectionIndex === 0 ? 0.5 : 1;
}
if (nextButton) {
nextButton.disabled = currentSectionIndex === sections.length - 1;
nextButton.style.opacity = currentSectionIndex === sections.length - 1 ? 0.5 : 1;
// Change text for last step
if (currentSectionIndex === sections.length - 2) {
nextButton.innerHTML = 'Finish <i class="fas fa-check"></i>';
} else {
nextButton.innerHTML = 'Next <i class="fas fa-arrow-right"></i>';
}
}
}
// Helper function to validate current section before proceeding
function validateCurrentSection() {
// Implement validation logic here based on the current section
// Return true if valid, false if not
return true;
}
// Add to initAgentFormCommon function if it exists
if (typeof window.initAgentFormCommon === 'function') {
const originalInit = window.initAgentFormCommon;
window.initAgentFormCommon = function(options) {
// Call the original initialization function
originalInit(options);
// Now initialize the wizard navigation
setActiveSection(0);
};
}
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

-291
View File
@@ -1,291 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Actions Playground</title>
{{template "old/views/partials/header"}}
</head>
<body>
{{template "old/views/partials/menu"}}
<!-- Toast for notifications -->
<div id="toast" class="toast">
<span id="toast-message"></span>
</div>
<div class="container mx-auto">
<header class="text-center mb-8">
<h1 class="text-4xl md:text-6xl font-bold">Actions Playground</h1>
<p class="mt-4 text-gray-400">Test and execute actions directly from the UI</p>
</header>
<section class="section-box mb-8">
<h2 class="mb-4">Select an Action</h2>
<div class="mb-4">
<label for="action-select" class="block mb-2">Available Actions:</label>
<select id="action-select" class="w-full">
<option value="">-- Select an action --</option>
<!-- Actions will be loaded here -->
</select>
</div>
</section>
<section id="config-section" class="section-box mb-8 hidden">
<h2 class="mb-4">Action Configuration</h2>
<form id="action-form">
<div class="mb-6">
<label for="config-json" class="block mb-2">Configuration (JSON):</label>
<textarea id="config-json" class="w-full" rows="5" placeholder='{"key": "value"}'>{}</textarea>
<p class="text-xs text-gray-400 mt-1">Enter JSON configuration for the action</p>
</div>
<div class="mb-6">
<label for="params-json" class="block mb-2">Parameters (JSON):</label>
<textarea id="params-json" class="w-full" rows="5" placeholder='{"key": "value"}'>{}</textarea>
<p class="text-xs text-gray-400 mt-1">Enter JSON parameters for the action</p>
</div>
<div class="flex justify-end">
<button type="submit" class="action-btn start-btn">
<i class="fas fa-play"></i> Execute Action
</button>
</div>
</form>
</section>
<section id="results-section" class="section-box mb-8 hidden">
<h2 class="mb-4">Action Results</h2>
<div id="action-results">
<!-- Results will appear here -->
</div>
</section>
<footer class="text-center text-gray-500 text-sm mb-8">
<p>&copy; 2025 LocalAGI.</p>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Load available actions
fetchActions();
// Handle action selection
document.getElementById('action-select').addEventListener('change', function() {
const actionId = this.value;
if (actionId) {
document.getElementById('config-section').classList.remove('hidden');
} else {
document.getElementById('config-section').classList.add('hidden');
}
// Hide results when changing actions
document.getElementById('results-section').classList.add('hidden');
});
// Handle form submission
document.getElementById('action-form').addEventListener('submit', function(e) {
e.preventDefault();
const actionId = document.getElementById('action-select').value;
if (actionId) {
executeAction(actionId);
} else {
showToast('Please select an action first', 'error');
}
});
});
function fetchActions() {
fetch('/api/actions')
.then(response => response.json())
.then(actions => {
const select = document.getElementById('action-select');
// Clear existing options except the first one
while (select.options.length > 1) {
select.remove(1);
}
if (actions.length === 0) {
const option = document.createElement('option');
option.text = 'No actions available';
option.disabled = true;
select.add(option);
return;
}
// Add options for each action
actions.forEach(actionId => {
const option = document.createElement('option');
option.value = actionId;
option.text = actionId; // Using actionId as display text
select.add(option);
});
})
.catch(error => {
console.error('Error fetching actions:', error);
showToast('Failed to load actions: ' + error.message, 'error');
const select = document.getElementById('action-select');
const option = document.createElement('option');
option.text = 'Error loading actions';
option.disabled = true;
// Clear existing options except the first one
while (select.options.length > 1) {
select.remove(1);
}
select.add(option);
});
}
function executeAction(actionId) {
// Get the JSON data from textareas
let config = {};
let params = {};
try {
const configText = document.getElementById('config-json').value.trim();
if (configText && configText !== '{}') {
config = JSON.parse(configText);
}
const paramsText = document.getElementById('params-json').value.trim();
if (paramsText && paramsText !== '{}') {
params = JSON.parse(paramsText);
}
} catch (error) {
showToast('Invalid JSON: ' + error.message, 'error');
return;
}
// Show the results section with loading indicator
const resultsSection = document.getElementById('results-section');
resultsSection.classList.remove('hidden');
const resultDiv = document.getElementById('action-results');
resultDiv.innerHTML = `
<div class="flex justify-center items-center py-8">
<div class="loader"></div>
</div>
`;
// Execute the action
fetch(`/api/action/${actionId}/run`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
config: config,
params: params
})
})
.then(response => {
return response.json();
})
.then(result => {
if (result.error) {
throw new Error(result.error);
}
// Display the results
showActionResult(result);
showToast('Action executed successfully!', 'success');
})
.catch(error => {
resultDiv.innerHTML = `
<div class="alert alert-error" style="display: block;">
<i class="fas fa-exclamation-circle mr-2"></i> Error: ${error.message}
</div>
`;
showToast('Error executing action', 'error');
});
}
function showActionResult(result) {
const resultDiv = document.getElementById('action-results');
let html = '';
// Display result
if (result.Result) {
html += `
<div class="mb-4">
<h4 class="text-lg mb-2" style="color: var(--secondary);">Result:</h4>
<div class="code-terminal">
<pre>${escapeHtml(result.Result)}</pre>
</div>
</div>
`;
}
// Display metadata if available
if (result.Metadata && Object.keys(result.Metadata).length > 0) {
html += `
<div class="mb-4">
<h4 class="text-lg mb-2" style="color: var(--secondary);">Metadata:</h4>
<div class="code-terminal">
<pre>${escapeHtml(JSON.stringify(result.Metadata, null, 2))}</pre>
</div>
</div>
`;
}
if (!html) {
html = '<p class="text-gray-400">No results returned from the action.</p>';
}
resultDiv.innerHTML = html;
// Scroll to results
resultDiv.scrollIntoView({ behavior: 'smooth' });
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function showToast(message, type) {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toast-message');
toastMessage.textContent = message;
toast.className = 'toast toast-' + type;
toast.classList.add('toast-visible');
setTimeout(() => {
toast.classList.remove('toast-visible');
}, 3000);
}
</script>
<style>
.loader {
width: 48px;
height: 48px;
border: 5px solid var(--tertiary);
border-bottom-color: transparent;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
@keyframes rotation {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.code-terminal {
margin-top: 0;
}
</style>
</body>
</html>
-332
View File
@@ -1,332 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agent List</title>
{{template "old/views/partials/header"}}
<style>
.avatar-placeholder {
width: 96px;
height: 96px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #2a2a2a, #3a3a3a);
color: var(--primary);
font-size: 1.5rem;
margin-bottom: 1rem;
border: 2px solid var(--primary);
box-shadow: var(--neon-glow);
position: relative;
overflow: hidden;
}
.avatar-placeholder::after {
content: "";
position: absolute;
width: 100%;
height: 4px;
background: var(--primary);
bottom: 0;
left: 0;
animation: loading-progress 2s infinite linear;
}
@keyframes loading-progress {
0% { width: 0; }
50% { width: 100%; }
100% { width: 0; }
}
.placeholder-text {
z-index: 1;
}
</style>
</head>
<body>
{{template "old/views/partials/menu"}}
<!-- Toast for notifications -->
<div id="toast" class="toast">
<span id="toast-message"></span>
</div>
<div class="container mx-auto">
<header class="text-center mb-8">
<h1 class="text-4xl md:text-6xl font-bold">Agent List</h1>
<p class="mt-4 text-gray-400">Manage and interact with your AI agents</p>
</header>
<div class="button-container justify-center mb-6">
<a href="/old/create" class="action-btn start-btn">
<i class="fas fa-plus-circle"></i> Add New Agent
</a>
<button id="toggle-import" class="action-btn" style="background: linear-gradient(135deg, var(--tertiary), #4a76a8);">
<i class="fas fa-file-import"></i> Import Agent
</button>
</div>
<section id="import-section" class="hidden mb-8">
<div class="section-box">
<h2>Import Agent</h2>
<!-- Response Messages Container -->
<div id="response-container">
<!-- Success Alert -->
<div id="success-alert" class="alert alert-success" style="display: none;">
Agent imported successfully! The page will refresh in a moment.
</div>
<!-- Error Alert -->
<div id="error-alert" class="alert alert-error" style="display: none;">
<span id="error-message">Error importing agent.</span>
</div>
</div>
<form id='import-form' hx-encoding='multipart/form-data' hx-post='/settings/import' hx-target="#response-container" hx-swap="none">
<div class="mb-4">
<label for="file" class="block mb-2">Select Agent File:</label>
<input type='file' name='file' id='file'>
</div>
<div class="flex items-center">
<button id="import-button" type="submit" class="action-btn">
<i class="fas fa-cloud-upload-alt"></i> Import Agent
</button>
</div>
</form>
</div>
</section>
<section class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{{ $status := .Status }}
{{ range .Agents }}
<div hx-ext="sse" data-agent-name="{{.}}" class="card">
<div class="flex flex-col items-center text-center p-4">
<div class="avatar-container mb-4">
<img src="/avatars/{{.}}.png" alt="{{.}}" class="w-24 h-24 rounded-full"
style="border: 2px solid var(--primary); box-shadow: var(--neon-glow); display: none;"
onload="this.style.display = 'block'; this.nextElementSibling.style.display = 'none';"
onerror="this.style.display = 'none'; this.nextElementSibling.style.display = 'flex';">
<div class="avatar-placeholder">
<span class="placeholder-text"><i class="fas fa-sync fa-spin"></i></span>
</div>
</div>
<h2>{{.}}</h2>
<div class="mb-4 flex items-center justify-center">
<span class="badge {{ if eq (index $status .) true }}badge-primary{{ else }}badge-secondary{{ end }} mr-2">
{{ if eq (index $status .) true }}Active{{ else }}Inactive{{ end }}
</span>
</div>
<div class="grid grid-cols-2 gap-2 w-full mb-4">
<a href="/old/status/{{.}}" class="action-btn flex items-center justify-center"
style="background: linear-gradient(135deg, #2a2a2a, #3a3a3a);">
<i class="fas fa-info-circle mr-2"></i> Status
</a>
<a href="/old/talk/{{.}}" class="action-btn flex items-center justify-center"
style="background: linear-gradient(135deg, #2a2a2a, #3a3a3a);">
<i class="fas fa-comments mr-2"></i> Talk
</a>
</div>
<div class="grid grid-cols-2 gap-2 w-full">
<button class="action-btn toggle-btn col-span-1"
data-agent="{{.}}"
data-active="{{ if eq (index $status .) true }}true{{ else }}false{{ end }}">
{{ if eq (index $status .) true }}
<i class="fas fa-pause"></i> Pause
{{ else }}
<i class="fas fa-play"></i> Start
{{ end }}
</button>
<a href="/old/settings/{{.}}" class="action-btn col-span-1 flex items-center justify-center"
style="background: linear-gradient(135deg, #2a2a2a, #3a3a3a);">
<i class="fas fa-cog"></i>
</a>
</div>
</div>
</div>
{{ end }}
</section>
<div class="user-info mt-8 mb-4">
<span></span>
<span class="timestamp"></span>
</div>
<footer class="text-center text-gray-500 text-sm mb-8">
<p>&copy; 2025 LocalAGI.</p>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Image loading handler
document.querySelectorAll('.avatar-container img').forEach(img => {
// Check if image is already cached
if (img.complete) {
if (img.naturalHeight === 0) {
// Image failed to load
img.style.display = 'none';
img.nextElementSibling.style.display = 'flex';
} else {
// Image loaded successfully
img.style.display = 'block';
img.nextElementSibling.style.display = 'none';
}
}
// onload and onerror handlers are already in the HTML
});
const importSection = document.getElementById('import-section');
const toggleImport = document.getElementById('toggle-import');
// Toggle import section visibility
toggleImport.addEventListener('click', function() {
importSection.classList.toggle('hidden');
// Add glitch effect when showing
if (!importSection.classList.contains('hidden')) {
importSection.style.animation = 'glitch 0.3s';
setTimeout(() => {
importSection.style.animation = '';
}, 300);
}
});
// Handle import form submission
document.getElementById('import-form').addEventListener('htmx:afterRequest', function(event) {
const xhr = event.detail.xhr;
const successAlert = document.getElementById('success-alert');
const errorAlert = document.getElementById('error-alert');
const errorMessage = document.getElementById('error-message');
// Hide both alerts initially
successAlert.style.display = 'none';
errorAlert.style.display = 'none';
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.status === "ok") {
// Show success message
successAlert.style.display = 'block';
showToast("Agent imported successfully!", "success");
// Refresh the page after a short delay
setTimeout(() => {
window.location.reload();
}, 2000);
} else if (response.error) {
// Show error message
errorMessage.textContent = response.error;
errorAlert.style.display = 'block';
showToast("Import failed: " + response.error, "error");
}
} catch (e) {
// Handle parsing error
errorMessage.textContent = "Invalid response format";
errorAlert.style.display = 'block';
showToast("Invalid response format", "error");
}
} else {
// Handle HTTP error
errorMessage.textContent = "Server error: " + xhr.status;
errorAlert.style.display = 'block';
showToast("Server error: " + xhr.status, "error");
}
});
// Handle toggle buttons - using pure JavaScript
document.querySelectorAll('.toggle-btn').forEach(button => {
button.addEventListener('click', function() {
const agent = this.getAttribute('data-agent');
const isActive = this.getAttribute('data-active') === 'true';
const endpoint = isActive ? `/api/agent/${agent}/pause` : `/api/agent/${agent}/start`;
// Add animation
this.style.animation = 'pulse 0.5s';
// Create a new XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('PUT', endpoint);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = () => {
// Clear animation
this.style.animation = '';
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.status === "ok") {
// Toggle the button state
const newState = !isActive;
this.setAttribute('data-active', newState ? 'true' : 'false');
// Update button text and icon
if (newState) {
this.innerHTML = '<i class="fas fa-pause"></i> Pause';
} else {
this.innerHTML = '<i class="fas fa-play"></i> Start';
}
// Show success toast
const action = isActive ? 'pause' : 'start';
showToast(`Agent "${agent}" ${action}ed successfully`, 'success');
// Update the status badge
updateAgentStatus(agent, newState);
} else if (response.error) {
// Show error toast
showToast(`Error: ${response.error}`, 'error');
}
} catch (e) {
// Handle parsing error
showToast("Invalid response format", 'error');
console.error("Error parsing response:", e);
}
} else {
// Handle HTTP error
showToast(`Server error: ${xhr.status}`, 'error');
}
};
xhr.onerror = () => {
// Clear animation
this.style.animation = '';
showToast("Network error occurred", 'error');
console.error("Network error occurred");
};
// Send the request
xhr.send(JSON.stringify({}));
});
});
});
// Function to update agent status in the UI
function updateAgentStatus(agentName, isOnline) {
// Find the card for this agent
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
const agentTitle = card.querySelector('h2').textContent;
if (agentTitle === agentName) {
// Update the badge
const badge = card.querySelector('.badge');
if (isOnline) {
badge.className = 'badge badge-primary mr-2';
badge.textContent = 'Active';
} else {
badge.className = 'badge badge-secondary mr-2';
badge.textContent = 'Inactive';
}
}
});
}
</script>
</body>
</html>
-91
View File
@@ -1,91 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Smart Agent Interface</title>
{{template "old/views/partials/header"}}
<style>
body { overflow: hidden; }
.chat-container { height: 90vh; display: flex; flex-direction: column; }
.chat-messages { overflow-y: auto; flex-grow: 1; }
.htmx-indicator{
opacity:0;
transition: opacity 10ms ease-in;
}
.htmx-request .htmx-indicator{
opacity:1
}
/* Loader (https://cssloaders.github.io/) */
.loader {
width: 12px;
height: 12px;
border-radius: 50%;
display: block;
margin:15px auto;
position: relative;
color: #FFF;
box-sizing: border-box;
animation: animloader 2s linear infinite;
}
@keyframes animloader {
0% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
25% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 2px; }
50% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 2px, -38px 0 0 -2px; }
75% { box-shadow: 14px 0 0 2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
100% { box-shadow: 14px 0 0 -2px, 38px 0 0 2px, -14px 0 0 -2px, -38px 0 0 -2px; }
}
</style>
</head>
<body class="bg-gray-900 p-4 text-white font-sans" hx-ext="sse" sse-connect="/sse/{{.Name}}">
{{template "old/views/partials/menu"}}
<div class="chat-container bg-gray-800 shadow-lg rounded-lg" >
<!-- Chat Header -->
<div class="border-b border-gray-700 p-4">
<h1 class="text-lg font-semibold">Talk to '{{.Name}}'</h1>
</div>
<!-- Chat Messages -->
<div class="chat-messages p-4">
<!-- Client Box -->
<div class="bg-gray-700 p-4">
<h2 class="text-sm font-semibold">Clients:</h2>
<div id="clients" class="text-sm text-gray-300">
<!-- Status updates dynamically here -->
<div sse-swap="clients"></div>
</div>
</div>
<!-- HUD Box -->
<div class="bg-gray-700 p-4">
<h2 class="text-sm font-semibold">Status:</h2>
<div id="hud" class="text-sm text-gray-300">
<!-- Status updates dynamically here -->
<div sse-swap="hud"></div>
</div>
</div>
<div sse-swap="messages" hx-swap="beforeend" id="messages" hx-on:htmx:after-settle="document.getElementById('messages').scrollIntoView(false)"></div>
</div>
<!-- Agent Status Box -->
<div class="bg-gray-700 p-4">
<h2 class="text-sm font-semibold">Agent:</h2>
<div id="agentStatus" class="text-sm text-gray-300">
<!-- Status updates dynamically here -->
<div sse-swap="status" ></div>
</div>
</div>
<!-- Message Input -->
<div class="p-4 border-t border-gray-700">
<div sse-swap="message_status"></div>
<input id="inputMessage" name="message" type="text" hx-post="/old/chat/{{.Name}}" hx-target="#results" hx-indicator=".htmx-indicator"
class="p-2 border rounded w-full bg-gray-600 text-white placeholder-gray-300" placeholder="Type a message..." _="on htmx:afterRequest set my value to ''">
<div class="my-2 htmx-indicator" ></div>
<div id="results" class="flex justify-center"></div>
</div>
</div>
</body>
</html>
-154
View File
@@ -1,154 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Create New Agent</title>
{{template "old/views/partials/header"}}
<script src="/old/public/js/wizard.js"></script>
<link rel="stylesheet" href="/old/public/css/wizard.css">
<script src="/old/public/js/connector-templates.js"></script>
<script src="/old/public/js/agent-form.js"></script>
</head>
<body>
{{template "old/views/partials/menu"}}
<div class="container">
<div class="section-box">
<h1>Create New Agent</h1>
<form id="create-agent-form" action="/api/agent/create" method="POST">
{{template "old/views/partials/agent-form" . }}
<button type="submit" id="create-button" data-original-text="<i class='fas fa-robot'></i> Create Agent">
<i class="fas fa-robot"></i> Create Agent
</button>
</form>
</div>
<!-- Response Messages Container -->
<div id="response-container">
<!-- Alert messages will be shown here -->
<div id="success-alert" class="alert alert-success" style="display: none;">
Agent created successfully! Redirecting to agent list...
</div>
<div id="error-alert" class="alert alert-error" style="display: none;">
<span id="error-message">Error creating agent.</span>
</div>
</div>
</div>
<!-- Toast notification container -->
<div id="toast" class="toast">
<span id="toast-message"></span>
</div>
<script>
const actions = `{{ range .Actions }}<option value="{{.}}">{{.}}</option>{{ end }}`;
const connectors = `{{ range .Connectors }}<option value="{{.}}">{{.}}</option>{{ end }}`;
const promptBlocks = `{{ range .PromptBlocks }}<option value="{{.}}">{{.}}</option>{{ end }}`;
document.addEventListener('DOMContentLoaded', function() {
// Initialize common form components
initAgentFormCommon({
actions: actions,
connectors: connectors,
promptBlocks: promptBlocks
});
// Form submission handling
const form = document.getElementById('create-agent-form');
form.addEventListener('submit', function(e) {
e.preventDefault();
// Show loading state
const createButton = document.getElementById('create-button');
const originalButtonText = createButton.innerHTML;
createButton.setAttribute('data-original-text', originalButtonText);
createButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating...';
createButton.disabled = true;
// Build a structured data object
const formData = new FormData(form);
const jsonData = AgentFormUtils.processFormData(formData);
// Process special fields
jsonData.connectors = AgentFormUtils.processConnectors(createButton);
if (jsonData.connectors === null) return; // Validation failed
jsonData.mcp_servers = AgentFormUtils.processMCPServers();
jsonData.actions = AgentFormUtils.processActions(createButton);
if (jsonData.actions === null) return; // Validation failed
jsonData.promptblocks = AgentFormUtils.processPromptBlocks(createButton);
if (jsonData.promptblocks === null) return; // Validation failed
// Send the structured data as JSON
fetch('/api/agent/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonData)
})
.then(response => response.json())
.then(data => {
const successAlert = document.getElementById('success-alert');
const errorAlert = document.getElementById('error-alert');
const errorMessage = document.getElementById('error-message');
// Hide both alerts initially
successAlert.style.display = 'none';
errorAlert.style.display = 'none';
if (data.status === "ok") {
// Show success toast
showToast('Agent created successfully!', 'success');
// Show success message
successAlert.style.display = 'block';
// Redirect to agent list page after a delay
setTimeout(() => {
window.location.href = '/old/agents';
}, 2000);
} else if (data.error) {
// Show error toast
showToast('Error: ' + data.error, 'error');
// Show error message
errorMessage.textContent = data.error;
errorAlert.style.display = 'block';
// Restore button state
createButton.innerHTML = originalButtonText;
createButton.disabled = false;
} else {
// Handle unexpected response format
showToast('Unexpected response format', 'error');
errorMessage.textContent = "Unexpected response format";
errorAlert.style.display = 'block';
// Restore button state
createButton.innerHTML = originalButtonText;
createButton.disabled = false;
}
})
.catch(error => {
// Handle network or other errors
showToast('Network error: ' + error.message, 'error');
const errorAlert = document.getElementById('error-alert');
const errorMessage = document.getElementById('error-message');
errorMessage.textContent = "Network error: " + error.message;
errorAlert.style.display = 'block';
// Restore button state
createButton.innerHTML = originalButtonText;
createButton.disabled = false;
});
});
});
</script>
</body>
</html>
-600
View File
@@ -1,600 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Create Agent Group</title>
{{template "old/views/partials/header"}}
<script src="/old/public/js/wizard.js"></script>
<link rel="stylesheet" href="/old/public/css/wizard.css">
<script src="/old/public/js/connector-templates.js"></script>
<script src="/old/public/js/agent-form.js"></script>
<style>
.agent-profile {
border: 1px solid var(--medium-bg);
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
background-color: var(--lighter-bg);
position: relative;
transition: all 0.3s ease;
}
.agent-profile:hover {
transform: translateY(-3px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
.agent-profile h3 {
color: var(--primary);
text-shadow: var(--neon-glow);
margin-top: 0;
margin-bottom: 15px;
border-bottom: 1px solid var(--medium-bg);
padding-bottom: 10px;
}
.agent-profile .description {
color: var(--text);
font-size: 0.9rem;
margin-bottom: 15px;
}
.agent-profile .system-prompt {
background-color: var(--darker-bg);
border-radius: 6px;
padding: 10px;
font-size: 0.85rem;
max-height: 150px;
overflow-y: auto;
margin-bottom: 10px;
white-space: pre-wrap;
}
.agent-profile.selected {
border: 2px solid var(--primary);
background-color: rgba(var(--primary-rgb), 0.1);
}
.agent-profile .select-checkbox {
position: absolute;
top: 10px;
right: 10px;
}
.page-section {
display: none;
animation: fadeIn 0.5s;
}
.page-section.active {
display: block;
}
.progress-container {
display: flex;
justify-content: center;
margin-bottom: 30px;
}
.progress-step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
padding: 0 20px;
}
.progress-step:not(:last-child)::after {
content: '';
position: absolute;
top: 12px;
right: -30px;
width: 60px;
height: 3px;
background-color: var(--medium-bg);
}
.progress-step.active:not(:last-child)::after {
background-color: var(--primary);
}
.step-circle {
width: 28px;
height: 28px;
border-radius: 50%;
background-color: var(--medium-bg);
display: flex;
justify-content: center;
align-items: center;
color: var(--text);
margin-bottom: 8px;
transition: all 0.3s ease;
}
.progress-step.active .step-circle {
background-color: var(--primary);
box-shadow: 0 0 10px var(--primary);
}
.step-label {
font-size: 0.9rem;
color: var(--muted-text);
transition: all 0.3s ease;
}
.progress-step.active .step-label {
color: var(--primary);
font-weight: bold;
}
.prompt-container {
margin-bottom: 30px;
}
.prompt-container textarea {
width: 100%;
min-height: 120px;
padding: 15px;
border-radius: 6px;
background-color: var(--lighter-bg);
border: 1px solid var(--medium-bg);
color: var(--text);
font-size: 1rem;
resize: vertical;
}
.action-buttons {
display: flex;
justify-content: space-between;
margin-top: 30px;
}
.select-all-container {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.loader {
display: none;
text-align: center;
margin: 40px 0;
}
.loader i {
color: var(--primary);
font-size: 2rem;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Make form elements auto in group mode */
#wizard-container .form-section {
height: auto;
overflow: visible;
}
.info-message {
background-color: rgba(var(--primary-rgb), 0.1);
border-left: 4px solid var(--primary);
padding: 15px;
margin: 20px 0;
border-radius: 0 8px 8px 0;
display: flex;
align-items: center;
}
.info-message i {
font-size: 1.5rem;
color: var(--primary);
margin-right: 15px;
}
.info-message-content {
flex: 1;
}
.info-message-content h4 {
margin-top: 0;
margin-bottom: 5px;
color: var(--primary);
}
.info-message-content p {
margin-bottom: 0;
}
</style>
</head>
<body>
{{template "old/views/partials/menu"}}
<div class="container">
<div class="section-box">
<h1>Create Agent Group</h1>
<!-- Progress Bar -->
<div class="progress-container">
<div class="progress-step active" data-step="1">
<div class="step-circle">1</div>
<div class="step-label">Generate Profiles</div>
</div>
<div class="progress-step" data-step="2">
<div class="step-circle">2</div>
<div class="step-label">Review & Select</div>
</div>
<div class="progress-step" data-step="3">
<div class="step-circle">3</div>
<div class="step-label">Configure Settings</div>
</div>
</div>
<!-- Step 1: Generate Profiles -->
<div id="step1" class="page-section active">
<h2>Generate Agent Profiles</h2>
<p>Describe the group of agents you want to create. Be specific about their roles, relationships, and purpose.</p>
<div class="prompt-container">
<textarea id="group-description" placeholder="Example: Create a team of agents for a software development project including a project manager, developer, tester, and designer. They should collaborate to build web applications."></textarea>
</div>
<div class="action-buttons">
<button type="button" id="generate-profiles-btn" class="action-btn">
<i class="fas fa-magic"></i> Generate Profiles
</button>
</div>
</div>
<!-- Loader -->
<div id="loader" class="loader">
<i class="fas fa-spinner fa-spin"></i>
<p>Generating agent profiles...</p>
</div>
<!-- Step 2: Review & Select Profiles -->
<div id="step2" class="page-section">
<h2>Review & Select Agent Profiles</h2>
<p>Select the agents you want to create. You can customize their details before creation.</p>
<div class="select-all-container">
<label for="select-all" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" id="select-all">
<span class="checkmark"></span>
</span>
Select All
</label>
</div>
<div id="agent-profiles-container">
<!-- Agent profiles will be generated here -->
</div>
<div class="action-buttons">
<button type="button" id="back-to-step1-btn" class="nav-btn">
<i class="fas fa-arrow-left"></i> Back
</button>
<button type="button" id="to-step3-btn" class="action-btn">
Continue <i class="fas fa-arrow-right"></i>
</button>
</div>
</div>
<!-- Step 3: Common Settings -->
<div id="step3" class="page-section">
<h2>Configure Common Settings</h2>
<p>Configure common settings for all selected agents. These settings will be applied to each agent.</p>
<form id="group-settings-form">
<!-- Informative message about profile data -->
<div class="info-message">
<i class="fas fa-info-circle"></i>
<div class="info-message-content">
<h4>Basic Information from Profiles</h4>
<p>The name, description, and system prompt for each agent will be taken from the profiles you selected in the previous step.</p>
</div>
</div>
<!-- Use the existing agent-form partial -->
<div id="group-agent-form">
{{template "old/views/partials/agent-form" . }}
</div>
</form>
<div class="action-buttons">
<button type="button" id="back-to-step2-btn" class="nav-btn">
<i class="fas fa-arrow-left"></i> Back
</button>
<button type="button" id="create-group-btn" class="action-btn" data-original-text="<i class='fas fa-users'></i> Create Agent Group">
<i class="fas fa-users"></i> Create Agent Group
</button>
</div>
</div>
</div>
<!-- Response Messages Container -->
<div id="response-container">
<!-- Alert messages will be shown here -->
<div id="success-alert" class="alert alert-success" style="display: none;">
Agents created successfully! Redirecting to agent list...
</div>
<div id="error-alert" class="alert alert-error" style="display: none;">
<span id="error-message">Error creating agents.</span>
</div>
</div>
</div>
<!-- Toast notification container -->
<div id="toast" class="toast">
<span id="toast-message"></span>
</div>
<script>
const actions = `{{ range .Actions }}<option value="{{.}}">{{.}}</option>{{ end }}`;
const connectors = `{{ range .Connectors }}<option value="{{.}}">{{.}}</option>{{ end }}`;
const promptBlocks = `{{ range .PromptBlocks }}<option value="{{.}}">{{.}}</option>{{ end }}`;
// Store generated agent profiles
let agentProfiles = [];
document.addEventListener('DOMContentLoaded', function() {
// Initialize the form components
initAgentFormCommon({
actions: actions,
connectors: connectors,
promptBlocks: promptBlocks
});
// Hide the Basic Information section
const basicSection = document.getElementById('basic-section');
if (basicSection) {
basicSection.style.display = 'none';
}
// Update the wizard navigation items to skip Basic Information
const basicNavItem = document.querySelector('.wizard-nav-item[data-target="basic-section"]');
if (basicNavItem) {
basicNavItem.style.display = 'none';
}
// Make sure Connectors section is active by default
const connectorsSection = document.getElementById('connectors-section');
if (connectorsSection) {
document.querySelectorAll('.form-section').forEach(section => {
section.classList.remove('active');
});
connectorsSection.classList.add('active');
}
// Update the active nav item
const connectorsNavItem = document.querySelector('.wizard-nav-item[data-target="connectors-section"]');
if (connectorsNavItem) {
document.querySelectorAll('.wizard-nav-item').forEach(item => {
item.classList.remove('active');
});
connectorsNavItem.classList.add('active');
}
// Update the current step label
const currentStepLabel = document.getElementById('currentStepLabel');
if (currentStepLabel) {
currentStepLabel.textContent = 'Connectors';
}
// Navigation between steps
const goToStep = (stepNumber) => {
// Hide all steps
document.querySelectorAll('.page-section').forEach(section => {
section.classList.remove('active');
});
// Show the target step
document.getElementById(`step${stepNumber}`).classList.add('active');
// Update progress bar
document.querySelectorAll('.progress-step').forEach(step => {
step.classList.remove('active');
if (parseInt(step.dataset.step) <= stepNumber) {
step.classList.add('active');
}
});
};
// Step 1: Generate Profiles
document.getElementById('generate-profiles-btn').addEventListener('click', function() {
const description = document.getElementById('group-description').value.trim();
if (!description) {
showToast('Please enter a description for your agent group', 'error');
return;
}
// Show loader
document.getElementById('loader').style.display = 'block';
document.getElementById('step1').style.display = 'none';
// Send request to generate profiles
fetch('/api/agent/group/generateProfiles', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ description: description })
})
.then(response => response.json())
.then(data => {
// Hide loader
document.getElementById('loader').style.display = 'none';
agentProfiles = Array.isArray(data) ? data : [];
if (agentProfiles.length === 0) {
showToast('No agent profiles were generated. Please try again with a more detailed description.', 'error');
document.getElementById('step1').style.display = 'block';
return;
}
// Render agent profiles
renderAgentProfiles();
// Go to step 2
goToStep(2);
})
.catch(error => {
document.getElementById('loader').style.display = 'none';
document.getElementById('step1').style.display = 'block';
showToast('Error generating profiles: ' + error.message, 'error');
console.error('Error:', error);
});
});
// Render agent profiles in step 2
function renderAgentProfiles() {
const container = document.getElementById('agent-profiles-container');
container.innerHTML = '';
agentProfiles.forEach((profile, index) => {
const profileElement = document.createElement('div');
profileElement.className = 'agent-profile';
profileElement.dataset.index = index;
profileElement.innerHTML = `
<label class="select-checkbox checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" class="profile-checkbox" checked>
<span class="checkmark"></span>
</span>
</label>
<h3>${profile.name}</h3>
<div class="description">${profile.description}</div>
<div class="system-prompt">${profile.system_prompt}</div>
`;
profileElement.querySelector('.profile-checkbox').addEventListener('change', function() {
profileElement.classList.toggle('selected', this.checked);
updateSelectAllCheckbox();
});
// Initially set as selected
profileElement.classList.add('selected');
container.appendChild(profileElement);
});
}
// Select all checkbox functionality
document.getElementById('select-all').addEventListener('change', function() {
const isChecked = this.checked;
document.querySelectorAll('.profile-checkbox').forEach(checkbox => {
checkbox.checked = isChecked;
checkbox.closest('.agent-profile').classList.toggle('selected', isChecked);
});
});
function updateSelectAllCheckbox() {
const checkboxes = document.querySelectorAll('.profile-checkbox');
const selectAllCheckbox = document.getElementById('select-all');
const allChecked = Array.from(checkboxes).every(checkbox => checkbox.checked);
const someChecked = Array.from(checkboxes).some(checkbox => checkbox.checked);
selectAllCheckbox.checked = allChecked;
selectAllCheckbox.indeterminate = !allChecked && someChecked;
}
// Navigation buttons
document.getElementById('back-to-step1-btn').addEventListener('click', () => goToStep(1));
document.getElementById('to-step3-btn').addEventListener('click', () => {
// Check if at least one profile is selected
const selectedProfiles = document.querySelectorAll('.profile-checkbox:checked');
if (selectedProfiles.length === 0) {
showToast('Please select at least one agent profile', 'error');
return;
}
goToStep(3);
});
document.getElementById('back-to-step2-btn').addEventListener('click', () => goToStep(2));
// Create group button
document.getElementById('create-group-btn').addEventListener('click', function() {
// Get selected profiles
const selectedProfileIndices = Array.from(document.querySelectorAll('.profile-checkbox:checked'))
.map(checkbox => parseInt(checkbox.closest('.agent-profile').dataset.index));
if (selectedProfileIndices.length === 0) {
showToast('Please select at least one agent profile', 'error');
return;
}
const selectedProfiles = selectedProfileIndices.map(index => agentProfiles[index]);
// Process form data for common settings
const formData = new FormData(document.getElementById('group-settings-form'));
const commonSettings = AgentFormUtils.processFormData(formData);
// Process special fields
commonSettings.connectors = AgentFormUtils.processConnectors(this);
if (commonSettings.connectors === null) return; // Validation failed
commonSettings.mcp_servers = AgentFormUtils.processMCPServers();
commonSettings.actions = AgentFormUtils.processActions(this);
if (commonSettings.actions === null) return; // Validation failed
commonSettings.promptblocks = AgentFormUtils.processPromptBlocks(this);
if (commonSettings.promptblocks === null) return; // Validation failed
// Show loading state
const createButton = document.getElementById('create-group-btn');
const originalButtonText = createButton.innerHTML;
createButton.setAttribute('data-original-text', originalButtonText);
createButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating...';
createButton.disabled = true;
// Create payload
const payload = {
agents: selectedProfiles,
agent_config: commonSettings
};
// Send request to create agents
fetch('/api/agent/group/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => {
const successAlert = document.getElementById('success-alert');
const errorAlert = document.getElementById('error-alert');
const errorMessage = document.getElementById('error-message');
// Hide both alerts initially
successAlert.style.display = 'none';
errorAlert.style.display = 'none';
if (data.status === "ok") {
// Show success toast
showToast(`${selectedProfiles.length} agent(s) created successfully!`, 'success');
// Show success message
successAlert.style.display = 'block';
// Redirect to agent list page after a delay
setTimeout(() => {
window.location.href = '/old/agents';
}, 2000);
} else if (data.error) {
// Show error toast
showToast('Error: ' + data.error, 'error');
// Show error message
errorMessage.textContent = data.error;
errorAlert.style.display = 'block';
// Restore button state
createButton.innerHTML = originalButtonText;
createButton.disabled = false;
} else {
// Handle unexpected response format
showToast('Unexpected response format', 'error');
errorMessage.textContent = "Unexpected response format";
errorAlert.style.display = 'block';
// Restore button state
createButton.innerHTML = originalButtonText;
createButton.disabled = false;
}
})
.catch(error => {
// Handle network or other errors
showToast('Network error: ' + error.message, 'error');
const errorAlert = document.getElementById('error-alert');
const errorMessage = document.getElementById('error-message');
errorMessage.textContent = "Network error: " + error.message;
errorAlert.style.display = 'block';
// Restore button state
createButton.innerHTML = originalButtonText;
createButton.disabled = false;
});
});
});
// Toast notification function - assuming this exists in your global scope
</script>
</body>
</html>
-224
View File
@@ -1,224 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Smart Assistant Dashboard</title>
{{template "old/views/partials/header"}}
<style>
.image-container {
display: flex;
justify-content: center;
margin: 40px 0;
position: relative;
}
.image-container img {
filter: drop-shadow(0 0 15px rgba(94, 0, 255, 0.6));
transition: all 0.5s ease;
}
.image-container:hover img {
transform: scale(1.05);
filter: drop-shadow(0 0 25px rgba(0, 255, 149, 0.8));
}
.image-container::after {
content: "";
position: absolute;
bottom: -15px;
left: 50%;
transform: translateX(-50%);
width: 40%;
height: 2px;
background: linear-gradient(90deg, transparent, var(--primary), var(--secondary), var(--tertiary), transparent);
}
.dashboard-title {
text-align: center;
margin-bottom: 40px;
font-size: 2.5rem;
letter-spacing: 2px;
position: relative;
display: inline-block;
left: 50%;
transform: translateX(-50%);
color: var(--primary);
text-shadow: var(--neon-glow);
animation: gentlePulse 3s infinite;
}
/* Gentle pulse animation for the title */
@keyframes gentlePulse {
0% { text-shadow: 0 0 7px var(--primary), 0 0 10px var(--primary); }
50% { text-shadow: 0 0 12px var(--primary), 0 0 20px var(--primary); }
100% { text-shadow: 0 0 7px var(--primary), 0 0 10px var(--primary); }
}
/* Subtle glitch effect for hover */
@keyframes subtleGlitch {
0% { transform: translateX(-50%); }
20% { transform: translateX(-50%) translate(-1px, 1px); }
40% { transform: translateX(-50%) translate(-1px, -1px); }
60% { transform: translateX(-50%) translate(1px, 1px); }
80% { transform: translateX(-50%) translate(1px, -1px); }
100% { transform: translateX(-50%); }
}
.cards-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 30px;
margin-top: 30px;
}
.user-info {
position: absolute;
top: 20px;
right: 20px;
}
.dashboard-stats {
display: flex;
justify-content: center;
gap: 20px;
margin: 20px 0 40px;
}
.stat-item {
background: rgba(17, 17, 17, 0.7);
border-radius: 8px;
padding: 15px;
min-width: 150px;
text-align: center;
position: relative;
overflow: hidden;
border: 1px solid rgba(94, 0, 255, 0.2);
}
.stat-item::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, var(--primary), var(--secondary));
opacity: 0.7;
}
.stat-count {
font-size: 2rem;
font-weight: 700;
color: var(--primary);
text-shadow: var(--neon-glow);
}
.stat-label {
font-size: 0.9rem;
color: #cccccc;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Badge positioning */
.card {
position: relative;
}
.badge {
position: absolute;
top: 15px;
right: 15px;
}
</style>
</head>
<body>
{{template "old/views/partials/menu"}}
<div class="container">
<div class="image-container">
<img src="/old/public/logo_1.png" width="250" alt="Company Logo">
</div>
<h1 class="dashboard-title">LocalAGI</h1>
<!-- Simple stats display -->
<div class="dashboard-stats">
<div class="stat-item">
<div class="stat-count">{{.Actions}}</div>
<div class="stat-label">Available Actions</div>
</div>
<div class="stat-item">
<div class="stat-count">{{.Connectors}}</div>
<div class="stat-label">Available Connectors</div>
</div>
<div class="stat-item">
<div class="stat-count">{{ .AgentCount }}</div>
<div class="stat-label">Agents</div>
</div>
</div>
<div class="cards-container">
<!-- Card for Agent List Page -->
<a href="/old/agents" class="card-link">
<div class="card">
<h2><i class="fas fa-robot"></i> Agent List</h2>
<p>View and manage your list of agents, including detailed profiles and statistics.</p>
</div>
</a>
<!-- Card for Create Agent -->
<a href="/old/create" class="card-link">
<div class="card">
<h2><i class="fas fa-plus-circle"></i> Create Agent</h2>
<p>Create a new intelligent agent with custom behaviors, connectors, and actions.</p>
</div>
</a>
<!-- Additional Cards for Future Features -->
<a href="#" class="card-link">
<div class="card">
<h2><i class="fas fa-chart-line"></i> Analytics</h2>
<p>View performance metrics and insights from your agent operations.</p>
<span class="badge badge-secondary">Coming Soon</span>
</div>
</a>
<a href="#" class="card-link">
<div class="card">
<h2><i class="fas fa-cogs"></i> Settings</h2>
<p>Configure system preferences and global settings for all agents.</p>
<span class="badge badge-secondary">Coming Soon</span>
</div>
</a>
</div>
</div>
<!-- Toast notification container -->
<div id="toast" class="toast">
<span id="toast-message"></span>
</div>
<script>
// Add the controlled glitch effect to dashboard title
document.addEventListener('DOMContentLoaded', function() {
const title = document.querySelector('.dashboard-title');
title.addEventListener('mouseover', function() {
// Use the more subtle glitch animation
this.style.animation = 'subtleGlitch 0.5s infinite';
});
title.addEventListener('mouseout', function() {
// Return to gentle pulse animation
this.style.animation = 'gentlePulse 3s infinite';
});
// Welcome toast notification
setTimeout(() => {
showToast('Welcome to Smart Assistant Dashboard', 'success');
}, 1000);
});
</script>
</body>
</html>
-215
View File
@@ -1,215 +0,0 @@
<!DOCTYPE html>
<html lang="en">
{{template "old/views/partials/header" .}}
<body class="bg-gradient-to-br from-gray-900 to-gray-950 text-gray-200">
<div class="flex flex-col min-h-screen">
{{template "old/views/partials/menu" .}}
<div class="container mx-auto px-4 py-8 flex-grow flex items-center justify-center">
<!-- Auth Card -->
<div class="max-w-md w-full bg-gray-800/90 border border-gray-700/50 rounded-xl overflow-hidden shadow-xl">
<div class="animation-container">
<div class="text-overlay">
<!-- <i class="fas fa-circle-nodes text-5xl text-blue-400 mb-2"></i> -->
</div>
</div>
<div class="p-8">
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-white">
<span class="bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-indigo-400">
Authorization Required
</span>
</h2>
<p class="text-gray-400 mt-2">Please enter your access token to continue</p>
</div>
<form id="login-form" class="space-y-6" onsubmit="login(); return false;">
<div>
<label for="token" class="block text-sm font-medium text-gray-300 mb-2">Access Token</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-key text-gray-500"></i>
</div>
<input
type="password"
id="token"
name="token"
placeholder="Enter your token"
class="bg-gray-700/50 border border-gray-600 text-white placeholder-gray-400 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5"
required
/>
</div>
</div>
<div>
<button
type="submit"
class="group w-full flex items-center justify-center bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white py-3 px-6 rounded-lg transition duration-300 ease-in-out transform hover:scale-[1.02] hover:shadow-lg font-medium"
>
<i class="fas fa-sign-in-alt mr-2"></i>
<span>Login</span>
<i class="fas fa-arrow-right opacity-0 group-hover:opacity-100 group-hover:translate-x-2 ml-2 transition-all duration-300"></i>
</button>
</div>
</form>
<div class="mt-8 pt-6 border-t border-gray-700/50 text-center text-sm text-gray-400">
<div class="flex items-center justify-center mb-2">
<i class="fas fa-shield-alt mr-2 text-blue-400"></i>
<span>Instance is token protected</span>
</div>
<p>Current time (UTC): <span id="current-time">{{.CurrentDate}}</span></p>
</div>
</div>
</div>
</div>
</div>
<script>
function login() {
const token = document.getElementById('token').value;
if (!token.trim()) {
// Show error with fading effect
const form = document.getElementById('login-form');
const errorMsg = document.createElement('div');
errorMsg.className = 'p-3 mt-4 bg-red-900/50 text-red-200 rounded-lg border border-red-700/50 text-sm flex items-center';
errorMsg.innerHTML = '<i class="fas fa-exclamation-circle mr-2"></i> Please enter a valid token';
// Remove any existing error message
const existingError = form.querySelector('.bg-red-900/50');
if (existingError) form.removeChild(existingError);
// Add new error message with animation
form.appendChild(errorMsg);
setTimeout(() => {
errorMsg.style.opacity = '0';
errorMsg.style.transition = 'opacity 0.5s ease';
setTimeout(() => errorMsg.remove(), 500);
}, 3000);
return;
}
var date = new Date();
date.setTime(date.getTime() + (24*60*60*1000));
document.cookie = `token=${token}; expires=${date.toGMTString()}; path=/`;
// Show loading state
const button = document.querySelector('button[type="submit"]');
const originalContent = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Authenticating...';
button.classList.add('bg-gray-600');
// Reload after short delay to show loading state
setTimeout(() => {
window.location.reload();
}, 800);
}
// Update current time
function updateCurrentTime() {
const timeElement = document.getElementById('current-time');
if (timeElement) {
const now = new Date();
const year = now.getUTCFullYear();
const month = String(now.getUTCMonth() + 1).padStart(2, '0');
const day = String(now.getUTCDate()).padStart(2, '0');
const hours = String(now.getUTCHours()).padStart(2, '0');
const minutes = String(now.getUTCMinutes()).padStart(2, '0');
const seconds = String(now.getUTCSeconds()).padStart(2, '0');
timeElement.textContent = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
}
// Initialize current time and update it every second
updateCurrentTime();
setInterval(updateCurrentTime, 1000);
// Add subtle particle animation to the background
document.addEventListener('DOMContentLoaded', function() {
const animContainer = document.querySelector('.animation-container');
if (animContainer) {
const canvas = document.createElement('canvas');
animContainer.appendChild(canvas);
const ctx = canvas.getContext('2d');
canvas.width = animContainer.offsetWidth;
canvas.height = animContainer.offsetHeight;
// Create particles
const particles = [];
const particleCount = 30;
for (let i = 0; i < particleCount; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
radius: Math.random() * 3 + 1,
color: `rgba(${Math.random() * 50 + 50}, ${Math.random() * 100 + 100}, ${Math.random() * 155 + 100}, ${Math.random() * 0.4 + 0.1})`,
speedX: Math.random() * 0.5 - 0.25,
speedY: Math.random() * 0.5 - 0.25
});
}
// Animation loop
function animate() {
requestAnimationFrame(animate);
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(particle => {
particle.x += particle.speedX;
particle.y += particle.speedY;
// Bounce off edges
if (particle.x < 0 || particle.x > canvas.width) {
particle.speedX = -particle.speedX;
}
if (particle.y < 0 || particle.y > canvas.height) {
particle.speedY = -particle.speedY;
}
// Draw particle
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
ctx.fillStyle = particle.color;
ctx.fill();
});
// Connect nearby particles with lines
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.strokeStyle = `rgba(100, 150, 255, ${0.1 * (1 - distance / 100)})`;
ctx.lineWidth = 1;
ctx.stroke();
}
}
}
}
// Start animation
animate();
// Resize handling
window.addEventListener('resize', () => {
canvas.width = animContainer.offsetWidth;
canvas.height = animContainer.offsetHeight;
});
}
});
</script>
</body>
</html>
-307
View File
@@ -1,307 +0,0 @@
<div class="agent-form-container">
<!-- Wizard Sidebar -->
<div class="wizard-sidebar">
<ul class="wizard-nav">
<li class="wizard-nav-item active" data-target="basic-section">
<i class="fas fa-info-circle"></i> Basic Information
</li>
<li class="wizard-nav-item" data-target="connectors-section">
<i class="fas fa-plug"></i> Connectors
</li>
<li class="wizard-nav-item" data-target="actions-section">
<i class="fas fa-bolt"></i> Actions
</li>
<li class="wizard-nav-item" data-target="mcp-section">
<i class="fas fa-server"></i> MCP Servers
</li>
<li class="wizard-nav-item" data-target="memory-section">
<i class="fas fa-memory"></i> Memory Settings
</li>
<li class="wizard-nav-item" data-target="model-section">
<i class="fas fa-robot"></i> Model Settings
</li>
<li class="wizard-nav-item" data-target="prompts-section">
<i class="fas fa-comment-alt"></i> Prompts & Goals
</li>
<li class="wizard-nav-item" data-target="advanced-section">
<i class="fas fa-cogs"></i> Advanced Settings
</li>
</ul>
</div>
<!-- Form Content Area -->
<div class="form-content-area">
<!-- Basic Information Section -->
<div class="form-section active" id="basic-section">
<h3 class="section-title">Basic Information</h3>
<div class="mb-4">
<label for="name">Name</label>
{{ if .Name }}
<input type="text" name="name" id="name" placeholder="Name" value="{{.Name}}" readonly >
{{ else }}
<input type="text" name="name" id="name" placeholder="Name">
{{ end }}
</div>
<div class="mb-4">
<label for="description">Description</label>
<textarea name="description" id="description" placeholder="Description"></textarea>
</div>
<div class="mb-4">
<label for="identity_guidance">Identity Guidance</label>
<textarea name="identity_guidance" id="identity_guidance" placeholder="Identity Guidance"></textarea>
</div>
<div class="mb-4">
<label for="random_identity" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="random_identity" id="random_identity">
<span class="checkmark"></span>
</span>
Random Identity
</label>
</div>
<div class="mb-4">
<label for="hud" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="hud" id="hud">
<span class="checkmark"></span>
</span>
HUD
</label>
</div>
</div>
<!-- Connectors Section -->
<div class="form-section" id="connectors-section">
<h3 class="section-title">Connectors</h3>
<div id="connectorsSection">
<!-- Connectors will be added here dynamically -->
</div>
<div class="button-container">
<button type="button" id="addConnectorButton" class="action-btn">
<i class="fas fa-plus-circle"></i> Add Connector
</button>
</div>
</div>
<!-- Actions Section -->
<div class="form-section" id="actions-section">
<h3 class="section-title">Actions</h3>
<div class="mb-4" id="action_box">
<!-- Actions will be added here dynamically -->
</div>
<div class="button-container">
<button id="action_button" type="button" class="action-btn">
<i class="fas fa-plus-circle"></i> Add Action
</button>
</div>
</div>
<!-- MCP Servers Section -->
<div class="form-section" id="mcp-section">
<h3 class="section-title">MCP Servers</h3>
<div id="mcpSection">
<!-- MCP servers will be added here dynamically -->
</div>
<div class="button-container">
<button type="button" id="addMCPButton" class="action-btn">
<i class="fas fa-plus-circle"></i> Add MCP Server
</button>
</div>
</div>
<!-- Memory Settings Section -->
<div class="form-section" id="memory-section">
<h3 class="section-title">Memory Settings</h3>
<div class="mb-4">
<label for="enable_kb" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="enable_kb" id="enable_kb">
<span class="checkmark"></span>
</span>
Enable Knowledge Base
</label>
</div>
<div class="mb-4">
<label for="kb_results">Knowledge Base Results</label>
<input type="number" name="kb_results" id="kb_results" placeholder="3">
</div>
<div class="mb-4">
<label for="long_term_memory" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="long_term_memory" id="long_term_memory">
<span class="checkmark"></span>
</span>
Long Term Memory
</label>
</div>
<div class="mb-4">
<label for="summary_long_term_memory" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="summary_long_term_memory" id="summary_long_term_memory">
<span class="checkmark"></span>
</span>
Long Term Memory (Summarize!)
</label>
</div>
</div>
<!-- Model Settings Section -->
<div class="form-section" id="model-section">
<h3 class="section-title">Model Settings</h3>
<div class="mb-4">
<label for="model">Model </label>
<input type="text" name="model" id="model" placeholder="Model name">
</div>
<div class="mb-4">
<label for="multimodal_model">Multimodal Model </label>
<input type="text" name="multimodal_model" id="multimodal_model" placeholder="Model name">
</div>
<div class="mb-4">
<label for="api_url">API URL </label>
<input type="text" name="api_url" id="api_url" placeholder="API URL">
</div>
<div class="mb-4">
<label for="api_key">API Key </label>
<input type="text" name="api_key" id="api_key" placeholder="API Key">
</div>
<div class="mb-4">
<label for="local_rag_url">LocalRAG API URL </label>
<input type="text" name="local_rag_url" id="local_rag_url" placeholder="LocalRAG API URL">
</div>
<div class="mb-4">
<label for="local_rag_api_key">LocalRAG API Key </label>
<input type="text" name="local_rag_api_key" id="local_rag_api_key" placeholder="LocalRAG API Key">
</div>
<div class="mb-4">
<label for="enable_reasoning" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="enable_reasoning" id="enable_reasoning">
<span class="checkmark"></span>
</span>
Enable Reasoning
</label>
</div>
</div>
<!-- Prompts & Goals Section -->
<div class="form-section" id="prompts-section">
<h3 class="section-title">Prompts & Goals</h3>
<div class="mb-4" id="dynamic_box">
<!-- Dynamic prompts will be added here dynamically -->
</div>
<div class="button-container">
<button id="dynamic_button" type="button" class="action-btn">
<i class="fas fa-plus-circle"></i> Add Dynamic Prompt
</button>
</div>
<div class="mb-4">
<label for="system_prompt">System Prompt</label>
<textarea name="system_prompt" id="system_prompt" placeholder="System prompt"></textarea>
</div>
<div class="mb-4">
<label for="permanent_goal">Permanent Goal</label>
<textarea name="permanent_goal" id="permanent_goal" placeholder="Permanent goal"></textarea>
</div>
</div>
<!-- Advanced Settings Section -->
<div class="form-section" id="advanced-section">
<h3 class="section-title">Advanced Settings</h3>
<div class="mb-4">
<label for="standalone_job" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="standalone_job" id="standalone_job">
<span class="checkmark"></span>
</span>
Standalone Job
</label>
</div>
<div class="mb-4">
<label for="initiate_conversations" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="initiate_conversations" id="initiate_conversations">
<span class="checkmark"></span>
</span>
Initiate Conversations
</label>
</div>
<div class="mb-4">
<label for="enable_planning" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="enable_planning" id="enable_planning">
<span class="checkmark"></span>
</span>
Enable Planning
</label>
</div>
<div class="mb-4">
<label for="can_stop_itself" class="checkbox-label">
<span class="checkbox-custom">
<input type="checkbox" name="can_stop_itself" id="can_stop_itself">
<span class="checkmark"></span>
</span>
Can Stop Itself
</label>
</div>
<div class="mb-4">
<label for="periodic_runs">Periodic Runs</label>
<input type="text" name="periodic_runs" id="periodic_runs" placeholder="Periodic Runs">
</div>
</div>
</div>
</div>
<!-- Add navigation controls at the bottom -->
<div class="wizard-controls">
<div class="wizard-controls-left">
<button type="button" id="prevSection" class="nav-btn">
<i class="fas fa-arrow-left"></i> Previous
</button>
</div>
<div class="wizard-controls-center">
<div class="progress-dots" id="progressDots">
<!-- Dots will be added dynamically via JS -->
</div>
<div class="progress-indicator">
<span id="currentStepLabel">Basic Information</span>
</div>
</div>
<div class="wizard-controls-right">
<button type="button" id="nextSection" class="nav-btn">
Next <i class="fas fa-arrow-right"></i>
</button>
</div>
</div>
-10
View File
@@ -1,10 +0,0 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=Permanent+Marker&display=swap" rel="stylesheet">
<script src="https://unpkg.com/htmx.org"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/sse.js"></script>
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
<link rel="stylesheet" href="/old/public/css/styles.css">
<script src="/old/public/js/common.js"></script>
-105
View File
@@ -1,105 +0,0 @@
<nav class="relative z-10 w-full" style="background-color: var(--darker-bg); border-bottom: 1px solid var(--medium-bg);">
<div class="px-6 sm:px-8 lg:px-10">
<div class="flex justify-between h-16 items-center">
<div class="flex items-center">
<!-- Logo container -->
<div class="flex-shrink-0">
<!-- Logo with glow effect -->
<a href="/old" class="flex items-center group">
<div class="relative">
<img src="/old/public/logo_2.png" alt="Logo" class="h-10 w-auto mr-4 transition-transform duration-300 group-hover:scale-105"
style="filter: drop-shadow(0 0 5px var(--primary));">
<!-- Animated scan line on hover -->
<div class="absolute inset-0 overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-var(--primary) to-transparent opacity-30"
style="height: 10px; animation: scanline 1.5s linear infinite;"></div>
</div>
</div>
<span class="text-xl font-bold transition-colors duration-300"
style="color: var(--primary); text-shadow: var(--neon-glow);">LocalAGI</span>
</a>
</div>
<div class="hidden md:block ml-10">
<div class="flex space-x-4">
<a href="/old" class="px-3 py-2 rounded-md text-lg font-medium text-white hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
<i class="fas fa-home mr-2"></i> Home
<!-- Underline animation -->
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
style="background: linear-gradient(90deg, var(--primary), var(--secondary));"></span>
</a>
<a href="/old/agents" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
<i class="fas fa-users mr-2"></i> Agent List
<!-- Underline animation -->
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
style="background: linear-gradient(90deg, var(--secondary), var(--tertiary));"></span>
</a>
<a href="/old/actions-playground" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
<i class="fas fa-bolt mr-2"></i> Actions Playground
<!-- Underline animation -->
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
style="background: linear-gradient(90deg, var(--tertiary), var(--primary));"></span>
</a>
<a href="/old/group-create" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
<i class="fas fa-users-cog mr-2"></i> Create Agent Group
<!-- Underline animation -->
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
style="background: linear-gradient(90deg, var(--secondary), var(--primary));"></span>
</a>
</div>
</div>
</div>
<div class="flex items-center space-x-4">
<!-- Status badge -->
<div class="hidden md:flex items-center">
<span class="flex items-center text-sm">
<span class="w-2 h-2 rounded-full mr-2"
style="background-color: var(--primary); box-shadow: 0 0 5px var(--primary); animation: pulse 2s infinite;"></span>
<span>State: <span style="color: var(--secondary); text-shadow: var(--pink-glow);">active</span></span>
</span>
</div>
</div>
<div class="md:hidden flex items-center">
<button class="text-gray-400 hover:text-white focus:outline-none focus:text-white transition duration-300"
style="text-shadow: var(--neon-glow);"
onclick="toggleMobileMenu()">
<i class="fas fa-bars fa-lg"></i>
</button>
</div>
</div>
</div>
<!-- Mobile menu, toggle based on menu state -->
<div id="mobile-menu" class="md:hidden hidden"
style="background-color: var(--darker-bg); border-top: 1px solid var(--medium-bg);">
<div class="px-2 pt-2 pb-3 space-y-1">
<a href="/old" class="block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-gray-800 transition duration-300"
style="border-left: 3px solid var(--primary);">
<i class="fas fa-home mr-2"></i> Home
</a>
<a href="/old/agents" class="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:bg-gray-800 transition duration-300"
style="border-left: 3px solid var(--secondary);">
<i class="fas fa-users mr-2"></i> Agent List
</a>
<a href="/old/actions-playground" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
<i class="fas fa-bolt mr-2"></i> Actions Playground
<!-- Underline animation -->
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
style="background: linear-gradient(90deg, var(--tertiary), var(--primary));"></span>
</a>
<a href="/old/group-create" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
<i class="fas fa-users-cog mr-2"></i> Create Agent Group
<!-- Underline animation -->
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
style="background: linear-gradient(90deg, var(--secondary), var(--primary));"></span>
</a>
</div>
</div>
</nav>
<br>
<script>
function toggleMobileMenu() {
const mobileMenu = document.getElementById('mobile-menu');
mobileMenu.classList.toggle('hidden');
}
</script>
-462
View File
@@ -1,462 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Agent settings {{.Name}}</title>
{{template "old/views/partials/header"}}
<script src="/old/public/js/wizard.js"></script>
<link rel="stylesheet" href="/old/public/css/wizard.css">
<script src="/old/public/js/connector-templates.js"></script>
<script src="/old/public/js/agent-form.js"></script>
</head>
<body>
{{template "old/views/partials/menu"}}
<!-- Toast notification container -->
<div id="toast" class="toast">
<span id="toast-message"></span>
</div>
<div class="container">
<header class="text-center mb-8">
<h1 class="text-3xl md:text-5xl font-bold">Agent settings - {{.Name}}</h1>
</header>
<div class="max-w-4xl mx-auto">
<!-- Agent Configuration Form Section -->
<div class="section-box">
<h2>Edit Agent Configuration</h2>
<form id="edit-agent-form">
<input type="hidden" name="name" id="name" value="{{.Name}}">
{{template "old/views/partials/agent-form" .}}
<button type="submit" id="update-button" class="action-btn" data-original-text="<i class='fas fa-save'></i> Update Agent">
<i class="fas fa-save"></i> Update Agent
</button>
</form>
</div>
<div class="section-box">
<h2>Agent Control</h2>
<div class="button-container">
<button
class="action-btn toggle-btn"
data-agent="{{.Name}}"
data-active="{{.Status}}">
{{if .Status}}
<i class="fas fa-pause"></i> Pause Agent
{{else}}
<i class="fas fa-play"></i> Start Agent
{{end}}
</button>
</div>
</div>
<div class="section-box">
<h2>Export Data</h2>
<p class="mb-4">Export your agent configuration for backup or transfer.</p>
<button
class="action-btn"
onclick="window.location.href='/old/settings/export/{{.Name}}'">
<i class="fas fa-file-export"></i> Export Configuration
</button>
</div>
<div class="section-box">
<h2>Danger Zone</h2>
<p class="mb-4">Permanently delete this agent and all associated data. This action cannot be undone.</p>
<button
class="action-btn"
style="background: linear-gradient(135deg, #ff4545, var(--secondary)); color: white;"
hx-delete="/api/agent/{{.Name}}"
hx-swap="none"
data-action="delete"
data-agent="{{.Name}}">
<i class="fas fa-trash-alt"></i> Delete Agent
</button>
</div>
<div class="user-info">
<span>Agent: {{.Name}}</span>
<span class="timestamp">Last modified: <span id="current-date"></span></span>
</div>
</div>
</div>
<script>
const actions = `{{ range .Actions }}<option value="{{.}}">{{.}}</option>{{ end }}`;
const connectors = `{{ range .Connectors }}<option value="{{.}}">{{.}}</option>{{ end }}`;
const promptBlocks = `{{ range .PromptBlocks }}<option value="{{.}}">{{.}}</option>{{ end }}`;
let agentConfig = null;
document.addEventListener('DOMContentLoaded', function() {
// Initialize common form components
initAgentFormCommon({
actions: actions,
connectors: connectors,
promptBlocks: promptBlocks
});
// Load agent configuration when page loads
loadAgentConfig();
// Add event listener for delete button
document.querySelectorAll('[data-action="delete"]').forEach(button => {
button.addEventListener('htmx:afterRequest', function(event) {
handleActionResponse(event, this);
});
});
// Handle toggle button
const toggleButton = document.querySelector('.toggle-btn');
if (toggleButton) {
toggleButton.addEventListener('click', function() {
const agent = this.getAttribute('data-agent');
const isActive = this.getAttribute('data-active') === "true";
const endpoint = isActive ? `/api/agent/${agent}/pause` : `/api/agent/${agent}/start`;
// Add animation
this.style.animation = 'pulse 0.5s';
// Create a new XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('PUT', endpoint);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = () => {
// Clear animation
this.style.animation = '';
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.status === "ok") {
// Toggle the button state
const newState = !isActive;
this.setAttribute('data-active', newState.toString());
// Update button text and icon
if (newState) {
this.innerHTML = '<i class="fas fa-pause"></i> Pause Agent';
} else {
this.innerHTML = '<i class="fas fa-play"></i> Start Agent';
}
// Show success toast
const action = isActive ? 'pause' : 'start';
showToast(`Agent "${agent}" ${action}ed successfully`, 'success');
} else if (response.error) {
// Show error toast
showToast(`Error: ${response.error}`, 'error');
}
} catch (e) {
// Handle parsing error
showToast("Invalid response format", 'error');
console.error("Error parsing response:", e);
}
} else {
// Handle HTTP error
showToast(`Server error: ${xhr.status}`, 'error');
}
};
xhr.onerror = () => {
// Clear animation
this.style.animation = '';
showToast("Network error occurred", 'error');
console.error("Network error occurred");
};
// Send the request
xhr.send(JSON.stringify({}));
});
}
// Set current date for timestamp
const now = new Date();
document.getElementById('current-date').textContent = now.toISOString().split('T')[0];
// Handle form submission for updating agent
const form = document.getElementById('edit-agent-form');
form.addEventListener('submit', function(e) {
e.preventDefault();
// Show a loading state
const updateButton = document.getElementById('update-button');
const originalButtonText = updateButton.innerHTML;
updateButton.setAttribute('data-original-text', originalButtonText);
updateButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Updating...';
updateButton.disabled = true;
// Build a structured data object
const formData = new FormData(form);
const jsonData = AgentFormUtils.processFormData(formData);
// Process special fields
jsonData.connectors = AgentFormUtils.processConnectors(updateButton);
if (jsonData.connectors === null) return; // Validation failed
jsonData.mcp_servers = AgentFormUtils.processMCPServers();
jsonData.actions = AgentFormUtils.processActions(updateButton);
if (jsonData.actions === null) return; // Validation failed
jsonData.promptblocks = AgentFormUtils.processPromptBlocks(updateButton);
if (jsonData.promptblocks === null) return; // Validation failed
console.log('Sending data:', jsonData);
// Send the structured data as JSON
fetch(`/api/agent/${jsonData.name}/config`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonData)
})
.then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error(err.error || `Server error: ${response.status}`);
});
}
return response.json();
})
.then(data => {
// Restore button state
updateButton.innerHTML = originalButtonText;
updateButton.disabled = false;
if (data.status === "ok") {
// Show success toast
showToast('Agent updated successfully!', 'success');
// Reload agent config to get updated values
setTimeout(() => {
loadAgentConfig();
}, 500);
} else if (data.error) {
// Show error toast
showToast('Error: ' + data.error, 'error');
} else {
// Handle unexpected response format
showToast('Unexpected response format', 'error');
}
})
.catch(error => {
// Handle network or other errors
showToast('Error: ' + error.message, 'error');
console.error('Update error:', error);
// Restore button state
updateButton.innerHTML = originalButtonText;
updateButton.disabled = false;
});
});
});
// Function to handle API responses for delete action
function handleActionResponse(event, button) {
const xhr = event.detail.xhr;
const action = button.getAttribute('data-action');
const agent = button.getAttribute('data-agent');
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.status === "ok") {
// Action successful
let message = "";
switch(action) {
case 'delete':
message = `Agent "${agent}" deleted successfully`;
// Redirect to agent list page after short delay for delete
setTimeout(() => {
window.location.href = "/old/agents";
}, 2000);
break;
default:
message = "Operation completed successfully";
}
// Show success message
showToast(message, 'success');
} else if (response.error) {
// Show error message
showToast(`Error: ${response.error}`, 'error');
}
} catch (e) {
// Handle JSON parsing error
showToast("Invalid response format", 'error');
}
} else {
// Handle HTTP error
showToast(`Server error: ${xhr.status}`, 'error');
}
}
// Load agent configuration from server
function loadAgentConfig() {
const agentName = document.getElementById('name').value;
fetch(`/api/agent/${agentName}/config`)
.then(response => {
if (!response.ok) {
throw new Error(`Failed to load agent config: ${response.status}`);
}
return response.json();
})
.then(data => {
agentConfig = data;
populateFormWithConfig(data);
showToast('Agent configuration loaded', 'success');
})
.catch(error => {
console.error('Error loading agent config:', error);
showToast('Error loading agent configuration: ' + error.message, 'error');
});
}
// Populate form with agent configuration
function populateFormWithConfig(config) {
// Clear existing dynamic sections
document.getElementById('connectorsSection').innerHTML = '';
document.getElementById('mcpSection').innerHTML = '';
document.getElementById('action_box').innerHTML = '';
document.getElementById('dynamic_box').innerHTML = '';
// Populate simple fields
document.getElementById('hud').checked = config.hud || false;
document.getElementById('enable_kb').checked = config.enable_kb || false;
document.getElementById('enable_reasoning').checked = config.enable_reasoning || false;
document.getElementById('kb_results').value = config.kb_results || '';
document.getElementById('standalone_job').checked = config.standalone_job || false;
document.getElementById('initiate_conversations').checked = config.initiate_conversations || false;
document.getElementById('enable_planning').checked = config.enable_planning || false;
document.getElementById('can_stop_itself').checked = config.can_stop_itself || false;
document.getElementById('random_identity').checked = config.random_identity || false;
document.getElementById('long_term_memory').checked = config.long_term_memory || false;
document.getElementById('summary_long_term_memory').checked = config.summary_long_term_memory || false;
document.getElementById('identity_guidance').value = config.identity_guidance || '';
document.getElementById('description').value = config.description || '';
document.getElementById('periodic_runs').value = config.periodic_runs || '';
document.getElementById('model').value = config.model || '';
document.getElementById('multimodal_model').value = config.multimodal_model || '';
document.getElementById('api_url').value = config.api_url || '';
document.getElementById('api_key').value = config.api_key || '';
document.getElementById('local_rag_url').value = config.local_rag_url || '';
document.getElementById('local_rag_api_key').value = config.local_rag_token || '';
document.getElementById('permanent_goal').value = config.permanent_goal || '';
document.getElementById('system_prompt').value = config.system_prompt || '';
// Populate connectors
if (config.connectors && Array.isArray(config.connectors)) {
config.connectors.forEach((connector, index) => {
// Add connector section
document.getElementById('addConnectorButton').click();
// Find the added connector elements
const connectorType = document.getElementById(`connectorType${index}`);
// Set values
if (connectorType) {
// First set the connector type
AgentFormUtils.setSelectValue(connectorType, connector.type);
// Parse the config if it's a string (from backend)
let configObj = connector.config;
if (typeof connector.config === 'string') {
try {
configObj = JSON.parse(connector.config);
} catch (e) {
console.error('Error parsing connector config:', e);
configObj = {}; // Fallback to empty object if parsing fails
}
}
// Now render the appropriate form for this connector type with the config values
AgentFormUtils.renderConnectorForm(index, connector.type, configObj);
}
});
}
// Populate MCP servers
if (config.mcp_servers && Array.isArray(config.mcp_servers)) {
config.mcp_servers.forEach((server, index) => {
// Add MCP server section
document.getElementById('addMCPButton').click();
// Find the added MCP server elements
const mcpURL = document.getElementById(`mcpURL${index}`);
const mcpToken = document.getElementById(`mcpToken${index}`);
// Set values
if (mcpURL) {
// If server is a string (old format), use it as URL
if (typeof server === 'string') {
mcpURL.value = server;
}
// If server is an object (new format), extract URL
else if (typeof server === 'object' && server !== null) {
mcpURL.value = server.url || '';
if (mcpToken && server.token) {
mcpToken.value = server.token;
}
}
}
});
}
// Populate actions
if (config.actions && Array.isArray(config.actions)) {
config.actions.forEach((action, index) => {
// Add action section
document.getElementById('action_button').click();
// Find the added action elements
const actionName = document.getElementById(`actionsName${index}`);
const actionConfig = document.getElementById(`actionsConfig${index}`);
// Set values
if (actionName) {
AgentFormUtils.setSelectValue(actionName, action.name);
}
if (actionConfig) {
// Format the config value
AgentFormUtils.formatConfigValue(actionConfig, action.config);
}
});
}
// Populate prompt blocks
if (config.promptblocks && Array.isArray(config.promptblocks)) {
config.promptblocks.forEach((block, index) => {
// Add prompt block section
document.getElementById('dynamic_button').click();
// Find the added prompt block elements
const promptName = document.getElementById(`promptName${index}`);
const promptConfig = document.getElementById(`promptConfig${index}`);
// Set values
if (promptName) {
AgentFormUtils.setSelectValue(promptName, block.name);
}
if (promptConfig) {
// Format the config value
AgentFormUtils.formatConfigValue(promptConfig, block.config);
}
});
}
}
</script>
</body>
</html>
-61
View File
@@ -1,61 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Smart Agent status</title>
{{template "old/views/partials/header"}}
<style>
body { overflow: hidden; }
.chat-container { height: 90vh; display: flex; flex-direction: column; }
.chat-messages { overflow-y: auto; flex-grow: 1; }
.htmx-indicator{
opacity:0;
transition: opacity 10ms ease-in;
}
.htmx-request .htmx-indicator{
opacity:1
}
/* Loader (https://cssloaders.github.io/) */
.loader {
width: 12px;
height: 12px;
border-radius: 50%;
display: block;
margin:15px auto;
position: relative;
color: #FFF;
box-sizing: border-box;
animation: animloader 2s linear infinite;
}
@keyframes animloader {
0% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
25% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 2px; }
50% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 2px, -38px 0 0 -2px; }
75% { box-shadow: 14px 0 0 2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
100% { box-shadow: 14px 0 0 -2px, 38px 0 0 2px, -14px 0 0 -2px, -38px 0 0 -2px; }
}
</style>
</head>
<body class="bg-gray-900 p-4 text-white font-sans" hx-ext="sse" sse-connect="/sse/{{.Name}}">
{{template "old/views/partials/menu"}}
<div class="chat-container bg-gray-800 shadow-lg rounded-lg" >
<!-- Chat Header -->
<div class="border-b border-gray-700 p-4">
<h1 class="text-lg font-semibold">{{.Name}}</h1>
</div>
<!-- Chat Messages -->
<div class="chat-messages p-4">
<div sse-swap="status" hx-swap="afterbegin" id="status"></div>
{{ range .History }}
<!-- Agent Status Box -->
<div class="bg-gray-700 p-4">
<h2 class="text-sm font-semibold">Agent:</h2>
<div id="agentStatus" class="text-sm text-gray-300">
Result: {{.Result}} Action: {{.Action}} Params: {{.Params}} Reasoning: {{.Reasoning}}
</div>
</div>
{{end}}
</div>
</div>
</body>
</html>
+7
View File
@@ -14,6 +14,7 @@ type Config struct {
LLMAPIKey string
LLMModel string
StateDir string
CustomActionsDir string
ConversationStoreDuration time.Duration
}
@@ -59,6 +60,12 @@ func WithLLMAPIKey(key string) Option {
}
}
func WithCustomActionsDir(dir string) Option {
return func(c *Config) {
c.CustomActionsDir = dir
}
}
func WithPool(pool *state.AgentPool) Option {
return func(c *Config) {
c.Pool = pool
+88 -210
View File
@@ -5,65 +5,51 @@
"name": "react-ui",
"dependencies": {
"highlight.js": "^11.11.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^6.0.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"react-router-dom": "^7.6.3",
"vite": "^7.0.3",
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0",
"eslint": "^9.39.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"react-router-dom": "^7.9.5",
"vite": "^7.1.12",
},
},
},
"packages": {
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/compat-data": ["@babel/compat-data@7.27.5", "", {}, "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg=="],
"@babel/core": ["@babel/core@7.27.4", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.4", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.4", "@babel/types": "^7.27.3", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g=="],
"@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="],
"@babel/generator": ["@babel/generator@7.27.5", "", { "dependencies": { "@babel/parser": "^7.27.5", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw=="],
"@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/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
"@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-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="],
"@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-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
"@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.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="],
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
"@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/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
@@ -71,9 +57,9 @@
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
"@babel/traverse": ["@babel/traverse@7.27.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA=="],
"@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
"@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="],
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="],
@@ -125,23 +111,23 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.2", "", { "os": "win32", "cpu": "x64" }, "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.6.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
"@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="],
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.3.0", "", {}, "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
"@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="],
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
"@eslint/js": ["@eslint/js@9.30.1", "", {}, "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg=="],
"@eslint/js": ["@eslint/js@9.39.1", "", {}, "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.1", "", { "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" } }, "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
@@ -151,7 +137,9 @@
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
@@ -159,49 +147,51 @@
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.19", "", {}, "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.43", "", {}, "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.0", "", { "os": "android", "cpu": "arm" }, "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.0", "", { "os": "android", "cpu": "arm" }, "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.40.0", "", { "os": "android", "cpu": "arm64" }, "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.50.0", "", { "os": "android", "cpu": "arm64" }, "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.50.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.50.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.40.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.50.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.40.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.50.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ=="],
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg=="],
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ=="],
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.40.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.50.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.40.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.50.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.50.0", "", { "os": "none", "cpu": "arm64" }, "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.50.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.50.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
@@ -211,15 +201,15 @@
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="],
"@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.6.0", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.19", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.0", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.43", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
@@ -269,11 +259,11 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.30.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.30.1", "@eslint/plugin-kit": "^0.3.1", "@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.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.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-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ=="],
"eslint": ["eslint@9.39.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.0", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "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.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.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-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg=="],
"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-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.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-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.20", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA=="],
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.24", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w=="],
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
@@ -295,7 +285,7 @@
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
@@ -311,7 +301,7 @@
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="],
"globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
@@ -381,7 +371,7 @@
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
@@ -389,21 +379,21 @@
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
"react-router": ["react-router@7.6.3", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA=="],
"react-router": ["react-router@7.9.5", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A=="],
"react-router-dom": ["react-router-dom@7.6.3", "", { "dependencies": { "react-router": "7.6.3" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-DiWJm9qdUAmiJrVWaeJdu4TKu13+iB/8IEi0EW/XgaHCjW/vWGrwzup0GVvaMteuZjKnh5bEvJP/K0MDnzawHw=="],
"react-router-dom": ["react-router-dom@7.9.5", "", { "dependencies": { "react-router": "7.9.5" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"rollup": ["rollup@4.40.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.40.0", "@rollup/rollup-android-arm64": "4.40.0", "@rollup/rollup-darwin-arm64": "4.40.0", "@rollup/rollup-darwin-x64": "4.40.0", "@rollup/rollup-freebsd-arm64": "4.40.0", "@rollup/rollup-freebsd-x64": "4.40.0", "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", "@rollup/rollup-linux-arm-musleabihf": "4.40.0", "@rollup/rollup-linux-arm64-gnu": "4.40.0", "@rollup/rollup-linux-arm64-musl": "4.40.0", "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-musl": "4.40.0", "@rollup/rollup-linux-s390x-gnu": "4.40.0", "@rollup/rollup-linux-x64-gnu": "4.40.0", "@rollup/rollup-linux-x64-musl": "4.40.0", "@rollup/rollup-win32-arm64-msvc": "4.40.0", "@rollup/rollup-win32-ia32-msvc": "4.40.0", "@rollup/rollup-win32-x64-msvc": "4.40.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w=="],
"rollup": ["rollup@4.50.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.0", "@rollup/rollup-android-arm64": "4.50.0", "@rollup/rollup-darwin-arm64": "4.50.0", "@rollup/rollup-darwin-x64": "4.50.0", "@rollup/rollup-freebsd-arm64": "4.50.0", "@rollup/rollup-freebsd-x64": "4.50.0", "@rollup/rollup-linux-arm-gnueabihf": "4.50.0", "@rollup/rollup-linux-arm-musleabihf": "4.50.0", "@rollup/rollup-linux-arm64-gnu": "4.50.0", "@rollup/rollup-linux-arm64-musl": "4.50.0", "@rollup/rollup-linux-loongarch64-gnu": "4.50.0", "@rollup/rollup-linux-ppc64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-musl": "4.50.0", "@rollup/rollup-linux-s390x-gnu": "4.50.0", "@rollup/rollup-linux-x64-gnu": "4.50.0", "@rollup/rollup-linux-x64-musl": "4.50.0", "@rollup/rollup-openharmony-arm64": "4.50.0", "@rollup/rollup-win32-arm64-msvc": "4.50.0", "@rollup/rollup-win32-ia32-msvc": "4.50.0", "@rollup/rollup-win32-x64-msvc": "4.50.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw=="],
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -419,7 +409,7 @@
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
@@ -427,7 +417,7 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"vite": ["vite@7.0.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "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-y2L5oJZF7bj4c0jgGYgBNSdIu+5HF+m68rn2cQXFbGoShdhV1phX9rbnxy9YXj82aS8MMsCLAAFkRxZeWdldrQ=="],
"vite": ["vite@7.1.12", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "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-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -437,39 +427,17 @@
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zod": ["zod@3.24.3", "", {}, "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg=="],
"zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
"zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="],
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
"@babel/core/@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="],
"@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.27.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA=="],
"@babel/generator/@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="],
"@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
"@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="],
"@babel/helper-member-expression-to-functions/@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="],
"@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
"@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
"@babel/helper-replace-supers/@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="],
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="],
"@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
"@babel/parser/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
"@babel/plugin-transform-private-methods/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="],
"@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="],
"@babel/template/@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="],
"@babel/traverse/@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="],
"@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"@babel/template/@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
@@ -479,71 +447,25 @@
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@types/babel__core/@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="],
"@types/babel__core/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
"@types/babel__generator/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
"@types/babel__template/@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="],
"@types/babel__template/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
"@types/babel__traverse/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
"eslint-plugin-react-hooks/@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="],
"eslint/@eslint/js": ["@eslint/js@9.39.0", "", {}, "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw=="],
"@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
"@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.5", "", { "dependencies": { "@babel/parser": "^7.27.5", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw=="],
"@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="],
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
"@babel/helper-create-class-features-plugin/@babel/traverse/@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-create-class-features-plugin/@babel/traverse/@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="],
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
"@babel/helper-create-class-features-plugin/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
"@babel/helper-member-expression-to-functions/@babel/traverse/@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-member-expression-to-functions/@babel/traverse/@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="],
"@babel/helper-member-expression-to-functions/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
"@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"@babel/helper-optimise-call-expression/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
"@babel/helper-optimise-call-expression/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"@babel/helper-replace-supers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
"@babel/helper-replace-supers/@babel/traverse/@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-replace-supers/@babel/traverse/@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="],
"@babel/helper-replace-supers/@babel/traverse/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
"@babel/helper-replace-supers/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@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-skip-transparent-expression-wrappers/@babel/traverse/@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="],
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
"@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
"@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"@eslint/eslintrc/espree/acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
@@ -565,52 +487,8 @@
"@types/babel__traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"eslint-plugin-react-hooks/@babel/core/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
"eslint-plugin-react-hooks/@babel/core/@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=="],
"eslint-plugin-react-hooks/@babel/core/@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=="],
"eslint-plugin-react-hooks/@babel/core/@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=="],
"eslint-plugin-react-hooks/@babel/core/@babel/helpers": ["@babel/helpers@7.27.0", "", { "dependencies": { "@babel/template": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg=="],
"eslint-plugin-react-hooks/@babel/core/@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="],
"eslint-plugin-react-hooks/@babel/core/@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="],
"eslint-plugin-react-hooks/@babel/core/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"@babel/helper-replace-supers/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"@babel/helper-replace-supers/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
"@babel/helper-replace-supers/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"eslint-plugin-react-hooks/@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"eslint-plugin-react-hooks/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.26.8", "", {}, "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ=="],
"eslint-plugin-react-hooks/@babel/core/@babel/helper-compilation-targets/@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="],
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="],
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"eslint-plugin-react-hooks/@babel/core/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"eslint-plugin-react-hooks/@babel/core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
"eslint-plugin-react-hooks/@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
}
}
+12 -12
View File
@@ -10,20 +10,20 @@
"preview": "vite preview"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"highlight.js": "^11.11.1"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^6.0.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"react-router-dom": "^7.6.3",
"vite": "^7.0.3"
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0",
"eslint": "^9.39.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"react-router-dom": "^7.9.5",
"vite": "^7.1.12"
}
}
+2 -1
View File
@@ -5,7 +5,7 @@ import ConfigForm from './ConfigForm';
* ActionForm component for configuring an action
* Renders action configuration forms based on field group metadata
*/
const ActionForm = ({ actions = [], onChange, onRemove, onAdd, fieldGroups = [] }) => {
const ActionForm = ({ actions = [], onChange, onRemove, onAdd, onPlay, fieldGroups = [] }) => {
const handleActionChange = (index, updatedAction) => {
onChange(index, updatedAction);
};
@@ -17,6 +17,7 @@ const ActionForm = ({ actions = [], onChange, onRemove, onAdd, fieldGroups = []
onChange={handleActionChange}
onRemove={onRemove}
onAdd={onAdd}
onPlay={onPlay}
itemType="action"
typeField="name"
addButtonText="Add Action"
+6 -6
View File
@@ -99,28 +99,28 @@ const AgentForm = ({
const handleAddDynamicPrompt = () => {
setFormData({
...formData,
dynamicPrompts: [
...(formData.dynamicPrompts || []),
dynamic_prompts: [
...(formData.dynamic_prompts || []),
{ type: '', config: '{}' }
]
});
};
const handleRemoveDynamicPrompt = (index) => {
const updatedDynamicPrompts = [...formData.dynamicPrompts];
const updatedDynamicPrompts = [...formData.dynamic_prompts];
updatedDynamicPrompts.splice(index, 1);
setFormData({
...formData,
dynamicPrompts: updatedDynamicPrompts,
dynamic_prompts: updatedDynamicPrompts,
});
};
const handleDynamicPromptChange = (index, updatedPrompt) => {
const updatedPrompts = [...formData.dynamicPrompts];
const updatedPrompts = [...formData.dynamic_prompts];
updatedPrompts[index] = updatedPrompt;
setFormData({
...formData,
dynamicPrompts: updatedPrompts
dynamic_prompts: updatedPrompts
});
};
+22 -10
View File
@@ -10,6 +10,7 @@ import FormFieldDefinition from './common/FormFieldDefinition';
* @param {Function} props.onChange - Callback when an item changes
* @param {Function} props.onRemove - Callback when an item is removed
* @param {Function} props.onAdd - Callback when a new item is added
* @param {Function} props.onPlay - Callback when a play button is clicked
* @param {String} props.itemType - Type of items being configured ('action', 'connector', etc.)
* @param {String} props.typeField - The field name that determines the item's type (e.g., 'name' for actions, 'type' for connectors)
* @param {String} props.addButtonText - Text for the add button
@@ -19,8 +20,9 @@ const ConfigForm = ({
items = [],
fieldGroups = [],
onChange,
onRemove,
onRemove,
onAdd,
onPlay,
itemType = 'item',
typeField = 'type',
addButtonText = 'Add Item',
@@ -89,15 +91,25 @@ const ConfigForm = ({
return (
<div key={index} className="config-item mb-4 card">
<div className="config-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
<h4 style={{ margin: 0 }}>{itemTypeLabel} #{index + 1}</h4>
<button
type="button"
className="action-btn delete-btn"
onClick={() => onRemove(index)}
>
<i className="fas fa-times"></i>
</button>
<div className="config-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem', gap: '1rem' }}>
<h4 style={{ margin: 0, flex: 1 }}>{itemTypeLabel} #{index + 1}</h4>
{/* Render Play button if onPlay handler is provided */}
{onPlay && (
<button
type="button"
className="action-btn"
onClick={() => onPlay(index)}
>
<i className="fas fa-play"></i>
</button>
)}
<button
type="button"
className="action-btn delete-btn"
onClick={() => onRemove(index)}
>
<i className="fas fa-times"></i>
</button>
</div>
<div className="config-type mb-3">
@@ -35,6 +35,18 @@ const ActionsSection = ({ formData, setFormData, metadata }) => {
});
};
// Handle the play button and open the action playground
const handleActionPlay = (index) => {
const action = formData.actions[index];
const searchParams = new URLSearchParams({
action: action.name,
config: action.config
});
// Open in new tab while staying in React Router context
window.open(`/app/actions-playground?${searchParams.toString()}`, '_blank');
};
return (
<div className="actions-section">
<h3>Actions</h3>
@@ -47,6 +59,7 @@ const ActionsSection = ({ formData, setFormData, metadata }) => {
onChange={handleActionChange}
onRemove={handleActionRemove}
onAdd={handleAddAction}
onPlay={handleActionPlay}
fieldGroups={metadata?.actions || []}
/>
</div>
@@ -67,7 +67,7 @@ const PromptsGoalsSection = ({
/>
<DynamicPromptForm
prompts={formData.dynamicPrompts || []}
prompts={formData.dynamic_prompts || []}
onAddPrompt={onAddPrompt}
onRemovePrompt={onRemovePrompt}
onChange={handleDynamicPromptChange}
+25 -1
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { useOutletContext, useNavigate } from 'react-router-dom';
import { useOutletContext, useNavigate, useSearchParams } from 'react-router-dom';
import { actionApi, agentApi } from '../utils/api';
import FormFieldDefinition from '../components/common/FormFieldDefinition';
import hljs from 'highlight.js/lib/core';
@@ -10,6 +10,7 @@ hljs.registerLanguage('json', json);
function ActionsPlayground() {
const { showToast } = useOutletContext();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [actions, setActions] = useState([]);
const [selectedAction, setSelectedAction] = useState('');
const [configJson, setConfigJson] = useState('{}');
@@ -47,6 +48,29 @@ function ActionsPlayground() {
fetchActions();
}, []);
// If query param 'action' is present, pre-select it once actions are loaded
useEffect(() => {
if (loadingActions) return;
const queryAction = searchParams.get('action');
if (!queryAction) return;
if (!selectedAction && actions.includes(queryAction)) {
setSelectedAction(queryAction);
}
}, [loadingActions, actions, searchParams, selectedAction]);
// If query param 'config' is present, pre-fill the configuration JSON
useEffect(() => {
const queryConfig = searchParams.get('config');
if (!queryConfig) return;
try {
const parsed = JSON.parse(queryConfig);
setConfigJson(JSON.stringify(parsed, null, 2));
} catch (err) {
console.error('Invalid config query parameter:', err);
showToast('Invalid config query parameter. Expected JSON.', 'error');
}
}, [searchParams, showToast]);
// Fetch agent metadata on mount
useEffect(() => {
const fetchAgentMetadata = async () => {
+41 -4
View File
@@ -16,6 +16,14 @@ function ObservableSummary({ observable }) {
creationChatMsg = lastMsg?.content || '';
}
}
// TODO:: Probably we have an array of two objects: [{type:"text", ...}, {type:"image_url", image_url: {url:"..."}}]
// We could display the image and text along with multimedia icon
if (typeof creationChatMsg === 'object') {
console.log("Multimedia message?", creationChatMsg);
creationChatMsg = 'Multimedia message';
}
// FunctionDefinition summary
let creationFunctionDef = '';
if (creation?.function_definition?.name) {
@@ -37,7 +45,6 @@ function ObservableSummary({ observable }) {
chatCompletion = { choices: completion.conversation.map(m => {
return { message: m }
}) }
console.log("converted conversation to choices", chatCompletion)
}
if (
@@ -106,7 +113,7 @@ function ObservableSummary({ observable }) {
!completionAgentState && !completionError && !completionFilter) {
return null;
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, margin: '2px 0 0 0' }}>
{/* CREATION */}
@@ -209,6 +216,7 @@ function AgentStatus() {
const [observableMap, setObservableMap] = useState({});
const [observableTree, setObservableTree] = useState([]);
const [expandedCards, setExpandedCards] = useState(new Map());
const [clearLoading, setClearLoading] = useState(false);
// Update document title
useEffect(() => {
@@ -282,7 +290,6 @@ function AgentStatus() {
sse.addEventListener('observable_update', (event) => {
const data = JSON.parse(event.data);
console.log(data);
setObservableMap(prevMap => {
const prev = prevMap[data.id] || {};
const updated = {
@@ -336,6 +343,26 @@ function AgentStatus() {
};
}, [name]);
const handleClearObservables = async () => {
if (clearLoading) return;
setClearLoading(true);
try {
const resp = await fetch(`/api/agent/${name}/observables`, { method: 'DELETE' });
if (!resp.ok) {
console.error('Failed to clear observables, status:', resp.status);
} else {
// Clear local state immediately
setObservableMap({});
setObservableTree([]);
setExpandedCards(new Map());
}
} catch (e) {
console.error('Error clearing observables:', e);
} finally {
setClearLoading(false);
}
};
// Helper function to safely convert any value to a displayable string
const formatValue = (value) => {
if (value === null || value === undefined) {
@@ -429,7 +456,17 @@ function AgentStatus() {
</div>
{observableTree.length > 0 && (
<div>
<h2>Observable Updates</h2>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2 style={{ margin: 0 }}>Observable Updates</h2>
<button
className="action-btn delete-btn"
onClick={handleClearObservables}
disabled={clearLoading}
title="Clear observable history"
>
{clearLoading ? 'Clearing' : 'Clear history'}
</button>
</div>
<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>
+14 -14
View File
@@ -22,12 +22,6 @@ import (
"github.com/mudler/LocalAGI/services"
)
//go:embed old/views/*
var viewsfs embed.FS
//go:embed old/public/*
var embeddedFiles embed.FS
//go:embed react-ui/dist/*
var reactUI embed.FS
@@ -40,12 +34,6 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
Browse: true,
}))
webapp.Use("/old/public", filesystem.New(filesystem.Config{
Root: http.FS(embeddedFiles),
PathPrefix: "/old/public",
Browse: true,
}))
if len(app.config.ApiKeys) > 0 {
kaConfig, err := GetKeyAuthConfig(app.config.ApiKeys)
if err != nil || kaConfig == nil {
@@ -183,10 +171,10 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
webapp.Put("/api/agent/:name/config", app.UpdateAgentConfig(pool))
// Metadata endpoint for agent configuration fields
webapp.Get("/api/agent/config/metadata", app.GetAgentConfigMeta())
webapp.Get("/api/agent/config/metadata", app.GetAgentConfigMeta(app.config.CustomActionsDir))
// Add endpoint for getting agent config metadata
webapp.Get("/api/meta/agent/config", app.GetAgentConfigMeta())
webapp.Get("/api/meta/agent/config", app.GetAgentConfigMeta(app.config.CustomActionsDir))
webapp.Post("/api/action/:name/definition", app.GetActionDefinition(pool))
webapp.Post("/api/action/:name/run", app.ExecuteAction(pool))
@@ -258,6 +246,7 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
})
})
// API endpoint to retrieve agent observables
webapp.Get("/api/agent/:name/observables", func(c *fiber.Ctx) error {
name := c.Params("name")
agent := pool.GetAgent(name)
@@ -273,6 +262,17 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
})
})
// API endpoint to clear agent observables
webapp.Delete("/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"})
}
agent.Observer().ClearHistory()
return c.JSON(fiber.Map{"Name": name, "cleared": true})
})
webapp.Post("/settings/import", app.ImportAgent(pool))
webapp.Get("/settings/export/:name", app.ExportAgent(pool))