Compare commits

..

66 Commits

Author SHA1 Message Date
Richard Palethorpe
5698d0b832 chore(tests): Mock LLM in tests for PRs
This saves time when testing on CPU which is the only sensible thing
to do on GitHub CI for PRs. For releases or once the commit is merged
we could use an external runner with GPU or just wait.

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-05-12 13:51:45 +01:00
Ettore Di Giacinto
c23e655f44 feat(agent): shared state, allow to track conversations globally (#148)
* feat(agent): shared state, allow to track conversations globally

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

* Cleanup

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

* track conversations initiated by the bot

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-11 22:23:01 +02:00
Ettore Di Giacinto
2b07dd79ec feat(telegram): add action to send telegram message (#147)
Signed-off-by: mudler <mudler@localai.io>
2025-05-11 19:17:20 +02:00
Ettore Di Giacinto
864bf8b94c fix(telegram): split long messages (#146)
Signed-off-by: mudler <mudler@localai.io>
2025-05-11 19:01:25 +02:00
mudler
60c53c8f3e Update README 2025-05-11 18:43:17 +02:00
mudler
cc0f5cbdcc Update README 2025-05-11 18:41:43 +02:00
mudler
8d527d6a09 Update README 2025-05-11 18:40:16 +02:00
Ettore Di Giacinto
e431bc234b feat(evaluation): add deep evaluation mechanism (#145)
* feat(evaluation): add deep evaluation mechanism

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

* consider whole conversation when evaluating

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Signed-off-by: mudler <mudler@localai.io>
2025-05-11 18:31:04 +02:00
Ettore Di Giacinto
289edb67a6 feat(call_agents): allow to specify whitelist and blacklist agents (#144)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-10 22:27:34 +02:00
Ettore Di Giacinto
324124e002 Update README.md 2025-05-07 22:23:31 +02:00
Ettore Di Giacinto
2bacac687f Update README.md 2025-05-07 22:23:04 +02:00
Ettore Di Giacinto
8b504a5e1e Update README.md 2025-05-07 22:22:41 +02:00
Martin Richardson
0e4e60cc15 fix(playground): convert fieldType number to string for json unmarshalling 2025-05-07 20:56:22 +01:00
Ettore Di Giacinto
fb1ab70650 fix(telegram): upload of images (#143)
* fix(telegram): upload of images

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

* Parse markdown

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

---------

Signed-off-by: mudler <mudler@localai.io>
2025-05-07 20:10:51 +02:00
Ettore Di Giacinto
94f4d350c9 feat(telegram): handle correctly generated multimedia and links (#141)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-06 22:38:33 +02:00
Ettore Di Giacinto
cc3fdecfc9 feat(telegram): show thought process (#140)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-06 22:24:11 +02:00
dependabot[bot]
c92acd670e chore(deps-dev): bump @types/react-dom from 19.1.2 to 19.1.3 in /webui/react-ui (#135)
chore(deps-dev): bump @types/react-dom in /webui/react-ui

Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 19.1.2 to 19.1.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/react-dom"
  dependency-version: 19.1.3
  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-05-06 10:12:31 +02:00
dependabot[bot]
0464a5b344 chore(deps): bump github.com/sashabaranov/go-openai from 1.39.0 to 1.39.1 (#134)
chore(deps): bump github.com/sashabaranov/go-openai

Bumps [github.com/sashabaranov/go-openai](https://github.com/sashabaranov/go-openai) from 1.39.0 to 1.39.1.
- [Release notes](https://github.com/sashabaranov/go-openai/releases)
- [Commits](https://github.com/sashabaranov/go-openai/compare/v1.39.0...v1.39.1)

---
updated-dependencies:
- dependency-name: github.com/sashabaranov/go-openai
  dependency-version: 1.39.1
  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-05-06 10:12:07 +02:00
dependabot[bot]
8bdb575bb2 chore(deps): bump github.com/go-telegram/bot from 1.14.2 to 1.15.0 (#132)
Bumps [github.com/go-telegram/bot](https://github.com/go-telegram/bot) from 1.14.2 to 1.15.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.14.2...v1.15.0)

---
updated-dependencies:
- dependency-name: github.com/go-telegram/bot
  dependency-version: 1.15.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-05-06 10:11:43 +02:00
Richard Palethorpe
f2c3b9dbdb feat(filters): Add configurable filters for incoming jobs
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-05-06 09:08:14 +01:00
Ettore Di Giacinto
02c6b5ad4e fix(reply): force replying without using tools (#131)
Signed-off-by: mudler <mudler@localai.io>
2025-05-05 18:33:53 +02:00
Ettore Di Giacinto
5e5224da25 fix(github): skip binary files
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-02 22:34:37 +02:00
Ettore Di Giacinto
c529f880d3 feat(github): add action to list and search files in a repository (#110)
Signed-off-by: mudler <mudler@localai.io>
2025-05-02 14:49:01 +02:00
Ettore Di Giacinto
18eb40ec14 fix(actions): make sure to initialize a config (#109)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-01 22:24:11 +02:00
Ettore Di Giacinto
904765591c fix(github/issue-editor): also update the title 2025-05-01 22:15:19 +02:00
dependabot[bot]
f726d3c3e5 chore(deps): bump github.com/sashabaranov/go-openai from 1.38.2 to 1.38.3 (#93)
chore(deps): bump github.com/sashabaranov/go-openai

Bumps [github.com/sashabaranov/go-openai](https://github.com/sashabaranov/go-openai) from 1.38.2 to 1.38.3.
- [Release notes](https://github.com/sashabaranov/go-openai/releases)
- [Commits](https://github.com/sashabaranov/go-openai/compare/v1.38.2...v1.38.3)

---
updated-dependencies:
- dependency-name: github.com/sashabaranov/go-openai
  dependency-version: 1.38.3
  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-05-01 22:08:08 +02:00
Ettore Di Giacinto
62ce629bf1 feat(github): add issue editor (#106)
* feat(github): add issue editor

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

* Small changes

---------

Signed-off-by: mudler <mudler@localai.io>
2025-05-01 22:07:41 +02:00
dependabot[bot]
9c555bd99f chore(deps): bump ubuntu from 22.04 to 24.04 (#107)
Bumps ubuntu from 22.04 to 24.04.

---
updated-dependencies:
- dependency-name: ubuntu
  dependency-version: '24.04'
  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-05-01 22:00:07 +02:00
Ettore Di Giacinto
5981109730 feat(github): add option to create PR from forks (#105)
* feat(github): add option to create PR from forks

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

* extend delays in waiting forking from github

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

---------

Signed-off-by: mudler <mudler@localai.io>
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-01 21:59:28 +02:00
Ettore Di Giacinto
087a5fbe0f feat(connectors): add support for Matrix (#82)
* feat(connectors): add support for Matrix

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

* make it functional

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

---------

Signed-off-by: mudler <mudler@localai.io>
2025-05-01 20:10:19 +02:00
Ettore Di Giacinto
67cb5937e7 fix: cleanup responses also when not picking any tool (#102)
* fix: process response also when no action is picked

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

* chore: rename method to be more meaningful

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-04-30 22:15:34 +02:00
Richard Palethorpe
8abf5512a4 fix(core): Add recursive loop detection and move loop detection (#101)
* fix(core): Add recursive loop detection and move loop detection

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

* fix(ci): Free up space after installation to avoid out of space error

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

---------

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-04-30 13:51:43 +00:00
mudler
45dd74d27c fix: multiline
Signed-off-by: mudler <mudler@localai.io>
2025-04-30 11:44:26 +02:00
Ettore Di Giacinto
1109b0a533 feat: add option to strip thinking from output (#100)
Signed-off-by: mudler <mudler@localai.io>
2025-04-30 11:05:39 +02:00
Richard Palethorpe
bd1b06f326 chore: Update all deps (#95)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-04-29 17:10:44 +02:00
Ettore Di Giacinto
7406db5882 feat: specify timeout (#97)
Signed-off-by: mudler <mudler@localai.io>
2025-04-29 17:10:11 +02:00
mudler
a1efa07b24 fix: set default timeout 2025-04-29 10:51:00 +02:00
Ettore Di Giacinto
29f7644577 feat: add deep research action (#91)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-04-29 08:46:55 +02:00
Ettore Di Giacinto
f3884c0244 chore(defaults): Enable reasoning by default (#89) 2025-04-27 17:42:53 +02:00
Richard Palethorpe
6516af6c34 Update README with videos
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-04-26 11:00:11 +01:00
Richard Palethorpe
77680c6fee feat(ui): Add summary details of each observable
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-04-26 11:00:11 +01:00
mudler
5faa599321 chore(mcpbox): add wget and curl
Signed-off-by: mudler <mudler@localai.io>
2025-04-25 18:19:57 +02:00
mudler
6209ededff fix(ci): add DEBIAN_FRONTEND
Signed-off-by: mudler <mudler@localai.io>
2025-04-25 18:11:13 +02:00
Richard Palethorpe
f6b6d5246c feat(ui): Action playground config and parameter forms
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-04-25 16:36:19 +01:00
Ettore Di Giacinto
b81624bfc2 chore(mcpbox): use ubuntu:24.04 as base (#86)
Signed-off-by: mudler <mudler@localai.io>
2025-04-25 17:06:50 +02:00
Ettore Di Giacinto
c1844f7230 chore(mcpbox): use dind (#85)
Signed-off-by: mudler <mudler@localai.io>
2025-04-25 17:05:56 +02:00
Richard Palethorpe
15efd2d527 fix(docker): Add mcpbox server to extended compose files (#84)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-04-25 17:04:06 +02:00
Ettore Di Giacinto
5e3bc0f89b fix(discord): automatically add 'Bot' prefix to token if missing (#83)
Signed-off-by: mudler <mudler@localai.io>
2025-04-25 16:20:29 +02:00
Ettore Di Giacinto
12209ab926 feat(browseragent): post screenshot on slack (#81)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-04-24 23:17:10 +02:00
dependabot[bot]
547e9cd0c4 chore(deps): bump actions/checkout from 2 to 4 (#44)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '4'
  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-04-24 22:45:29 +02:00
Richard Palethorpe
6a1e536ca7 Update README.md (#80)
Add observability screenshot and bullet point. Also update strap line and descriptions at the top to try and describe the benefit of this software
2025-04-24 18:01:58 +02:00
Ettore Di Giacinto
eb8663ada1 feat: local MCP server support (#61)
* wip

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

* Add groups to mcpbox

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

* Add mcpbox dockerfile and entrypoint

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

* Attach mcp stdio box to agent

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

* Add to dockerfile

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

* Attach to config

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

* Attach to ui

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

* Revert "Attach to ui"

This reverts commit 088d0c47e87ee8f84297e47d178fb7384bbe6d45.

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

* add one-time process, attach to UI the mcp server json configuration

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

* quality of life improvements

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

* fixes

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

* Make it working, expose MCP prepare script to UI

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

* Add container image to CI builds

* Wire mcpbox to tests

* Improve setup'

* Not needed anymore, using tests

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

* fix: do not override actions

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

* chore(tests): fix env var

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Signed-off-by: mudler <mudler@localai.io>
2025-04-24 16:39:20 +02:00
Richard Palethorpe
ce997d2425 fix: Handle state on agent restart and update observables (#75)
Keep some agent start across restarts, such as the SSE manager and
observer. This allows restarts to be shown on the state page and also
allows avatars to be kept when reconfiguring the agent.

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

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

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

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

* chore(tests): add timeout

---------

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

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

* Apply suggestions from code review

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

* collect errors

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

---------

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

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

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

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

* fix: simplify tests to run faster

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

---------

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

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

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

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

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

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

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

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

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

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

* feat(ui): Add structured observability events

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

---------

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-04-18 17:32:43 +02:00
Ettore Di Giacinto
70e749b53a fix(github*): pass by correctly owner and repository (#54)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-04-17 23:01:19 +02:00
Ettore Di Giacinto
784a4c7969 fix(githubreader): do not use pointers (#53)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-04-17 22:45:24 +02:00
112 changed files with 7042 additions and 1078 deletions

View File

@@ -84,3 +84,77 @@ jobs:
#tags: ${{ steps.prep.outputs.tags }} #tags: ${{ steps.prep.outputs.tags }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} 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
platforms: linux/amd64
push: true
#tags: ${{ steps.prep.outputs.tags }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v4
- run: | - run: |
# Add Docker's official GPG key: # Add Docker's official GPG key:
sudo apt-get update sudo apt-get update
@@ -30,17 +30,29 @@ jobs:
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \ $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin make
docker version docker version
docker run --rm hello-world docker run --rm hello-world
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: '>=1.17.0' 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 - name: Run tests
run: | run: |
sudo apt-get update && sudo apt-get install -y make if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
make tests make tests-mock
else
make tests
fi
#sudo mv coverage/coverage.txt coverage.txt #sudo mv coverage/coverage.txt coverage.txt
#sudo chmod 777 coverage.txt #sudo chmod 777 coverage.txt

49
.github/workflows/tests_fragile.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Run Fragile Go Tests
on:
pull_request:
branches:
- '**'
concurrency:
group: ci-non-blocking-tests-${{ github.head_ref || github.ref }}-${{ github.repository }}
cancel-in-progress: true
jobs:
llm-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- run: |
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install -y 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
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

49
Dockerfile.mcpbox Normal file
View File

@@ -0,0 +1,49 @@
# 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"]

View File

@@ -1,15 +1,22 @@
GOCMD?=go GOCMD?=go
IMAGE_NAME?=webui IMAGE_NAME?=webui
MCPBOX_IMAGE_NAME?=mcpbox
ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
prepare-tests: .PHONY: tests tests-mock cleanup-tests
prepare-tests: build-mcpbox
docker compose up -d --build 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: cleanup-tests:
docker compose down docker compose down
tests: prepare-tests tests: prepare-tests
LOCALAGI_MODEL="arcee-agent" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --fail-fast -v -r ./... LOCALAGI_MCPBOX_URL="http://localhost:9090" LOCALAGI_MODEL="gemma-3-12b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --fail-fast -v -r ./...
tests-mock: prepare-tests
LOCALAGI_MCPBOX_URL="http://localhost:9090" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --fail-fast -v -r ./...
run-nokb: run-nokb:
$(MAKE) run KBDISABLEINDEX=true $(MAKE) run KBDISABLEINDEX=true
@@ -23,10 +30,16 @@ build: webui/react-ui/dist
.PHONY: run .PHONY: run
run: webui/react-ui/dist run: webui/react-ui/dist
$(GOCMD) run ./ LOCALAGI_MCPBOX_URL="http://localhost:9090" $(GOCMD) run ./
build-image: build-image:
docker build -t $(IMAGE_NAME) -f Dockerfile.webui . docker build -t $(IMAGE_NAME) -f Dockerfile.webui .
image-push: image-push:
docker push $(IMAGE_NAME) 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

241
README.md
View File

@@ -2,7 +2,7 @@
<img src="./webui/react-ui/public/logo_1.png" alt="LocalAGI Logo" width="220"/> <img src="./webui/react-ui/public/logo_1.png" alt="LocalAGI Logo" width="220"/>
</p> </p>
<h3 align="center"><em>Your AI. Your Hardware. Your Rules.</em></h3> <h3 align="center"><em>Your AI. Your Hardware. Your Rules</em></h3>
<div align="center"> <div align="center">
@@ -11,11 +11,14 @@
[![GitHub stars](https://img.shields.io/github/stars/mudler/LocalAGI)](https://github.com/mudler/LocalAGI/stargazers) [![GitHub stars](https://img.shields.io/github/stars/mudler/LocalAGI)](https://github.com/mudler/LocalAGI/stargazers)
[![GitHub issues](https://img.shields.io/github/issues/mudler/LocalAGI)](https://github.com/mudler/LocalAGI/issues) [![GitHub issues](https://img.shields.io/github/issues/mudler/LocalAGI)](https://github.com/mudler/LocalAGI/issues)
Try on [![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/LocalAGI_bot)
</div> </div>
We empower you building AI Agents that you can run locally, without coding. Create customizable AI assistants, automations, chat bots and agents that run 100% locally. No need for agentic Python libraries or cloud service keys, just bring your GPU (or even just CPU) and a web browser.
**LocalAGI** is a powerful, self-hostable AI Agent platform designed for maximum privacy and flexibility. A complete drop-in replacement for OpenAI's Responses APIs with advanced agentic capabilities. No clouds. No data leaks. Just pure local AI that works on consumer-grade hardware (CPU and GPU). **LocalAGI** is a powerful, self-hostable AI Agent platform that allows you to design AI automations without writing code. A complete drop-in replacement for OpenAI's Responses APIs with advanced agentic capabilities. No clouds. No data leaks. Just pure local AI that works on consumer-grade hardware (CPU and GPU).
## 🛡️ Take Back Your Privacy ## 🛡️ Take Back Your Privacy
@@ -37,6 +40,7 @@ LocalAGI ensures your data stays exactly where you want it—on your hardware. N
- 🖼 **Multimodal Support**: Ready for vision, text, and more. - 🖼 **Multimodal Support**: Ready for vision, text, and more.
- 🔧 **Extensible Custom Actions**: Easily script dynamic agent behaviors in Go (interpreted, no compilation!). - 🔧 **Extensible Custom Actions**: Easily script dynamic agent behaviors in Go (interpreted, no compilation!).
- 🛠 **Fully Customizable Models**: Use your own models or integrate seamlessly with [LocalAI](https://github.com/mudler/LocalAI). - 🛠 **Fully Customizable Models**: Use your own models or integrate seamlessly with [LocalAI](https://github.com/mudler/LocalAI).
- 📊 **Observability**: Monitor agent status and view detailed observable updates in real-time.
## 🛠️ Quickstart ## 🛠️ Quickstart
@@ -68,6 +72,13 @@ Now you can access and manage your agents at [http://localhost:8080](http://loca
Still having issues? see this Youtube video: https://youtu.be/HtVwIxW3ePg Still having issues? see this Youtube video: https://youtu.be/HtVwIxW3ePg
## Videos
[![Creating a basic agent](https://img.youtube.com/vi/HtVwIxW3ePg/mqdefault.jpg)](https://youtu.be/HtVwIxW3ePg)
[![Agent Observability](https://img.youtube.com/vi/v82rswGJt_M/mqdefault.jpg)](https://youtu.be/v82rswGJt_M)
[![Filters and Triggers](https://youtu.be/d_we-AYksSw/mqdefault.jpg)](https://youtu.be/d_we-AYksSw)
## 📚🆕 Local Stack Family ## 📚🆕 Local Stack Family
🆕 LocalAI is now part of a comprehensive suite of AI tools designed to work together: 🆕 LocalAI is now part of a comprehensive suite of AI tools designed to work together:
@@ -114,7 +125,7 @@ LocalAGI supports multiple hardware configurations through Docker Compose profil
- Supports text, multimodal, and image generation models - Supports text, multimodal, and image generation models
- Run with: `docker compose -f docker-compose.nvidia.yaml up` - Run with: `docker compose -f docker-compose.nvidia.yaml up`
- Default models: - Default models:
- Text: `arcee-agent` - Text: `gemma-3-12b-it-qat`
- Multimodal: `minicpm-v-2_6` - Multimodal: `minicpm-v-2_6`
- Image: `sd-1.5-ggml` - Image: `sd-1.5-ggml`
- Environment variables: - Environment variables:
@@ -130,7 +141,7 @@ LocalAGI supports multiple hardware configurations through Docker Compose profil
- Supports text, multimodal, and image generation models - Supports text, multimodal, and image generation models
- Run with: `docker compose -f docker-compose.intel.yaml up` - Run with: `docker compose -f docker-compose.intel.yaml up`
- Default models: - Default models:
- Text: `arcee-agent` - Text: `gemma-3-12b-it-qat`
- Multimodal: `minicpm-v-2_6` - Multimodal: `minicpm-v-2_6`
- Image: `sd-1.5-ggml` - Image: `sd-1.5-ggml`
- Environment variables: - Environment variables:
@@ -161,7 +172,7 @@ docker compose -f docker-compose.intel.yaml up
``` ```
If no models are specified, it will use the defaults: If no models are specified, it will use the defaults:
- Text model: `arcee-agent` - Text model: `gemma-3-12b-it-qat`
- Multimodal model: `minicpm-v-2_6` - Multimodal model: `minicpm-v-2_6`
- Image model: `sd-1.5-ggml` - Image model: `sd-1.5-ggml`
@@ -179,14 +190,6 @@ Good (relatively small) models that have been tested are:
- **✓ Effortless Setup**: Simple Docker compose setups and pre-built binaries. - **✓ Effortless Setup**: Simple Docker compose setups and pre-built binaries.
- **✓ Feature-Rich**: From planning to multimodal capabilities, connectors for Slack, MCP support, LocalAGI has it all. - **✓ Feature-Rich**: From planning to multimodal capabilities, connectors for Slack, MCP support, LocalAGI has it all.
## 🌐 The Local Ecosystem
LocalAGI is part of the powerful Local family of privacy-focused AI tools:
- [**LocalAI**](https://github.com/mudler/LocalAI): Run Large Language Models locally.
- [**LocalRecall**](https://github.com/mudler/LocalRecall): Retrieval-Augmented Generation with local storage.
- [**LocalAGI**](https://github.com/mudler/LocalAGI): Deploy intelligent AI agents securely and privately.
## 🌟 Screenshots ## 🌟 Screenshots
### Powerful Web UI ### Powerful Web UI
@@ -194,6 +197,8 @@ LocalAGI is part of the powerful Local family of privacy-focused AI tools:
![Web UI Dashboard](https://github.com/user-attachments/assets/a40194f9-af3a-461f-8b39-5f4612fbf221) ![Web UI Dashboard](https://github.com/user-attachments/assets/a40194f9-af3a-461f-8b39-5f4612fbf221)
![Web UI Agent Settings](https://github.com/user-attachments/assets/fb3c3e2a-cd53-4ca8-97aa-c5da51ff1f83) ![Web UI Agent Settings](https://github.com/user-attachments/assets/fb3c3e2a-cd53-4ca8-97aa-c5da51ff1f83)
![Web UI Create Group](https://github.com/user-attachments/assets/102189a2-0fba-4a1e-b0cb-f99268ef8062) ![Web UI Create Group](https://github.com/user-attachments/assets/102189a2-0fba-4a1e-b0cb-f99268ef8062)
![Web UI Agent Observability](https://github.com/user-attachments/assets/f7359048-9d28-4cf1-9151-1f5556ce9235)
### Connectors Ready-to-Go ### Connectors Ready-to-Go
@@ -256,6 +261,158 @@ go build -o localagi
./localagi ./localagi
``` ```
### Using as a Library
LocalAGI can be used as a Go library to programmatically create and manage AI agents. Let's start with a simple example of creating a single agent:
<details>
<summary><strong>Basic Usage: Single Agent</strong></summary>
```go
import (
"github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/core/types"
)
// Create a new agent with basic configuration
agent, err := agent.New(
agent.WithModel("gpt-4"),
agent.WithLLMAPIURL("http://localhost:8080"),
agent.WithLLMAPIKey("your-api-key"),
agent.WithSystemPrompt("You are a helpful assistant."),
agent.WithCharacter(agent.Character{
Name: "my-agent",
}),
agent.WithActions(
// Add your custom actions here
),
agent.WithStateFile("./state/my-agent.state.json"),
agent.WithCharacterFile("./state/my-agent.character.json"),
agent.WithTimeout("10m"),
agent.EnableKnowledgeBase(),
agent.EnableReasoning(),
)
if err != nil {
log.Fatal(err)
}
// Start the agent
go func() {
if err := agent.Run(); err != nil {
log.Printf("Agent stopped: %v", err)
}
}()
// Stop the agent when done
agent.Stop()
```
This basic example shows how to:
- Create a single agent with essential configuration
- Set up the agent's model and API connection
- Configure basic features like knowledge base and reasoning
- Start and stop the agent
</details>
<details>
<summary><strong>Advanced Usage: Agent Pools</strong></summary>
For managing multiple agents, you can use the AgentPool system:
```go
import (
"github.com/mudler/LocalAGI/core/state"
"github.com/mudler/LocalAGI/core/types"
)
// Create a new agent pool
pool, err := state.NewAgentPool(
"default-model", // default model name
"default-multimodal-model", // default multimodal model
"image-model", // image generation model
"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
return func(ctx context.Context, pool *AgentPool) []types.Action {
return []types.Action{
// Add your custom actions here
}
}
},
func(config *AgentConfig) []Connector {
// Define connectors for agents
return []Connector{
// Add your custom connectors here
}
},
func(config *AgentConfig) []DynamicPrompt {
// Define dynamic prompts for agents
return []DynamicPrompt{
// Add your custom prompts here
}
},
func(config *AgentConfig) types.JobFilters {
// Define job filters for agents
return types.JobFilters{
// Add your custom filters here
}
},
"10m", // timeout
true, // enable conversation logs
)
// Create a new agent in the pool
agentConfig := &AgentConfig{
Name: "my-agent",
Model: "gpt-4",
SystemPrompt: "You are a helpful assistant.",
EnableKnowledgeBase: true,
EnableReasoning: true,
// Add more configuration options as needed
}
err = pool.CreateAgent("my-agent", agentConfig)
// Start all agents
err = pool.StartAll()
// Get agent status
status := pool.GetStatusHistory("my-agent")
// Stop an agent
pool.Stop("my-agent")
// Remove an agent
err = pool.Remove("my-agent")
```
</details>
<details>
<summary><strong>Available Features</strong></summary>
Key features available through the library:
- **Single Agent Management**: Create and manage individual agents with basic configuration
- **Agent Pool Management**: Create, start, stop, and remove multiple agents
- **Configuration**: Customize agent behavior through AgentConfig
- **Actions**: Define custom actions for agents to perform
- **Connectors**: Add custom connectors for external services
- **Dynamic Prompts**: Create dynamic prompt templates
- **Job Filters**: Implement custom job filtering logic
- **Status Tracking**: Monitor agent status and history
- **State Persistence**: Automatic state saving and loading
For more details about available configuration options and features, refer to the [Agent Configuration Reference](#agent-configuration-reference) section.
</details>
### Development ### Development
The development workflow is similar to the source build, but with additional steps for hot reloading of the frontend: The development workflow is similar to the source build, but with additional steps for hot reloading of the frontend:
@@ -282,7 +439,8 @@ cd ../.. && go run main.go
Link your agents to the services you already use. Configuration examples below. Link your agents to the services you already use. Configuration examples below.
### GitHub Issues <details>
<summary><strong>GitHub Issues</strong></summary>
```json ```json
{ {
@@ -292,8 +450,10 @@ Link your agents to the services you already use. Configuration examples below.
"botUserName": "bot-username" "botUserName": "bot-username"
} }
``` ```
</details>
### Discord <details>
<summary><strong>Discord</strong></summary>
After [creating your Discord bot](https://discordpy.readthedocs.io/en/stable/discord.html): After [creating your Discord bot](https://discordpy.readthedocs.io/en/stable/discord.html):
@@ -305,8 +465,10 @@ After [creating your Discord bot](https://discordpy.readthedocs.io/en/stable/dis
``` ```
> Don't forget to enable "Message Content Intent" in Bot(tab) settings! > Don't forget to enable "Message Content Intent" in Bot(tab) settings!
> Enable " Message Content Intent " in the Bot tab! > Enable " Message Content Intent " in the Bot tab!
</details>
### Slack <details>
<summary><strong>Slack</strong></summary>
Use the included `slack.yaml` manifest to create your app, then configure: Use the included `slack.yaml` manifest to create your app, then configure:
@@ -319,9 +481,10 @@ Use the included `slack.yaml` manifest to create your app, then configure:
- Create Oauth token bot token from "OAuth & Permissions" -> "OAuth Tokens for Your Workspace" - Create Oauth token bot token from "OAuth & Permissions" -> "OAuth Tokens for Your Workspace"
- Create App level token (from "Basic Information" -> "App-Level Tokens" ( scope connections:writeRoute authorizations:read )) - Create App level token (from "Basic Information" -> "App-Level Tokens" ( scope connections:writeRoute authorizations:read ))
</details>
<details>
### Telegram <summary><strong>Telegram</strong></summary>
Get a token from @botfather, then: Get a token from @botfather, then:
@@ -330,8 +493,10 @@ Get a token from @botfather, then:
"token": "your-bot-father-token" "token": "your-bot-father-token"
} }
``` ```
</details>
### IRC <details>
<summary><strong>IRC</strong></summary>
Connect to IRC networks: Connect to IRC networks:
@@ -344,10 +509,12 @@ Connect to IRC networks:
"alwaysReply": "false" "alwaysReply": "false"
} }
``` ```
</details>
## REST API ## REST API
### Agent Management <details>
<summary><strong>Agent Management</strong></summary>
| Endpoint | Method | Description | Example | | Endpoint | Method | Description | Example |
|----------|--------|-------------|---------| |----------|--------|-------------|---------|
@@ -362,8 +529,10 @@ Connect to IRC networks:
| `/api/meta/agent/config` | GET | Get agent configuration metadata | | | `/api/meta/agent/config` | GET | Get agent configuration metadata | |
| `/settings/export/:name` | GET | Export agent config | [Example](#export-agent) | | `/settings/export/:name` | GET | Export agent config | [Example](#export-agent) |
| `/settings/import` | POST | Import agent config | [Example](#import-agent) | | `/settings/import` | POST | Import agent config | [Example](#import-agent) |
</details>
### Actions and Groups <details>
<summary><strong>Actions and Groups</strong></summary>
| Endpoint | Method | Description | Example | | Endpoint | Method | Description | Example |
|----------|--------|-------------|---------| |----------|--------|-------------|---------|
@@ -371,8 +540,10 @@ Connect to IRC networks:
| `/api/action/:name/run` | POST | Execute an action | | | `/api/action/:name/run` | POST | Execute an action | |
| `/api/agent/group/generateProfiles` | POST | Generate group profiles | | | `/api/agent/group/generateProfiles` | POST | Generate group profiles | |
| `/api/agent/group/create` | POST | Create a new agent group | | | `/api/agent/group/create` | POST | Create a new agent group | |
</details>
### Chat Interactions <details>
<summary><strong>Chat Interactions</strong></summary>
| Endpoint | Method | Description | Example | | Endpoint | Method | Description | Example |
|----------|--------|-------------|---------| |----------|--------|-------------|---------|
@@ -380,6 +551,7 @@ Connect to IRC networks:
| `/api/notify/:name` | POST | Send notification to agent | [Example](#notify-agent) | | `/api/notify/:name` | POST | Send notification to agent | [Example](#notify-agent) |
| `/api/sse/:name` | GET | Real-time agent event stream | [Example](#agent-sse-stream) | | `/api/sse/:name` | GET | Real-time agent event stream | [Example](#agent-sse-stream) |
| `/v1/responses` | POST | Send message & get response | [OpenAI's Responses](https://platform.openai.com/docs/api-reference/responses/create) | | `/v1/responses` | POST | Send message & get response | [OpenAI's Responses](https://platform.openai.com/docs/api-reference/responses/create) |
</details>
<details> <details>
<summary><strong>Curl Examples</strong></summary> <summary><strong>Curl Examples</strong></summary>
@@ -467,11 +639,13 @@ curl -X POST "http://localhost:3000/api/notify/my-agent" \
curl -N -X GET "http://localhost:3000/api/sse/my-agent" curl -N -X GET "http://localhost:3000/api/sse/my-agent"
``` ```
Note: For proper SSE handling, you should use a client that supports SSE natively. Note: For proper SSE handling, you should use a client that supports SSE natively.
</details> </details>
### Agent Configuration Reference ### Agent Configuration Reference
<details>
<summary><strong>Configuration Structure</strong></summary>
The agent configuration defines how an agent behaves and what capabilities it has. You can view the available configuration options and their descriptions by using the metadata endpoint: The agent configuration defines how an agent behaves and what capabilities it has. You can view the available configuration options and their descriptions by using the metadata endpoint:
```bash ```bash
@@ -504,6 +678,25 @@ Here's an example of the agent configuration structure:
"summary_long_term_memory": false "summary_long_term_memory": false
} }
``` ```
</details>
<details>
<summary><strong>Environment Configuration</strong></summary>
LocalAGI supports environment configurations. Note that these environment variables needs to be specified in the localagi container in the docker-compose file to have effect.
| Variable | What It Does |
|----------|--------------|
| `LOCALAGI_MODEL` | Your go-to model |
| `LOCALAGI_MULTIMODAL_MODEL` | Optional model for multimodal capabilities |
| `LOCALAGI_LLM_API_URL` | OpenAI-compatible API server URL |
| `LOCALAGI_LLM_API_KEY` | API authentication |
| `LOCALAGI_TIMEOUT` | Request timeout settings |
| `LOCALAGI_STATE_DIR` | Where state gets stored |
| `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 |
</details>
## LICENSE ## LICENSE

38
cmd/mcpbox/main.go Normal file
View File

@@ -0,0 +1,38 @@
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)
}

View File

@@ -81,7 +81,7 @@ func (a *CustomAction) Plannable() bool {
return true return true
} }
func (a *CustomAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (a *CustomAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
v, err := a.i.Eval(fmt.Sprintf("%s.Run", a.config["name"])) v, err := a.i.Eval(fmt.Sprintf("%s.Run", a.config["name"]))
if err != nil { if err != nil {
return types.ActionResult{}, err return types.ActionResult{}, err
@@ -95,6 +95,11 @@ func (a *CustomAction) Run(ctx context.Context, params types.ActionParams) (type
func (a *CustomAction) Definition() types.ActionDefinition { func (a *CustomAction) Definition() types.ActionDefinition {
if a.i == nil {
xlog.Error("Interpreter is not initialized for custom action", "action", a.config["name"])
return types.ActionDefinition{}
}
v, err := a.i.Eval(fmt.Sprintf("%s.Definition", a.config["name"])) v, err := a.i.Eval(fmt.Sprintf("%s.Definition", a.config["name"]))
if err != nil { if err != nil {
xlog.Error("Error getting custom action definition", "error", err) xlog.Error("Error getting custom action definition", "error", err)

View File

@@ -76,7 +76,7 @@ return []string{"foo"}
Description: "A test action", Description: "A test action",
})) }))
runResult, err := customAction.Run(context.Background(), types.ActionParams{ runResult, err := customAction.Run(context.Background(), nil, types.ActionParams{
"Foo": "bar", "Foo": "bar",
}) })
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())

View File

@@ -21,7 +21,7 @@ type GoalResponse struct {
Achieved bool `json:"achieved"` Achieved bool `json:"achieved"`
} }
func (a *GoalAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) { func (a *GoalAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil return types.ActionResult{}, nil
} }

View File

@@ -22,7 +22,7 @@ type IntentResponse struct {
Reasoning string `json:"reasoning"` Reasoning string `json:"reasoning"`
} }
func (a *IntentAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) { func (a *IntentAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil return types.ActionResult{}, nil
} }

View File

@@ -19,7 +19,7 @@ type ConversationActionResponse struct {
Message string `json:"message"` Message string `json:"message"`
} }
func (a *ConversationAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) { func (a *ConversationAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil return types.ActionResult{}, nil
} }

View File

@@ -16,7 +16,7 @@ func NewStop() *StopAction {
type StopAction struct{} type StopAction struct{}
func (a *StopAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) { func (a *StopAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil return types.ActionResult{}, nil
} }

View File

@@ -30,7 +30,7 @@ type PlanSubtask struct {
Reasoning string `json:"reasoning"` Reasoning string `json:"reasoning"`
} }
func (a *PlanAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) { func (a *PlanAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil return types.ActionResult{}, nil
} }

View File

@@ -20,7 +20,7 @@ type ReasoningResponse struct {
Reasoning string `json:"reasoning"` Reasoning string `json:"reasoning"`
} }
func (a *ReasoningAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) { func (a *ReasoningAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil return types.ActionResult{}, nil
} }

View File

@@ -22,7 +22,7 @@ type ReplyResponse struct {
Message string `json:"message"` Message string `json:"message"`
} }
func (a *ReplyAction) Run(context.Context, types.ActionParams) (string, error) { func (a *ReplyAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (string, error) {
return "no-op", nil return "no-op", nil
} }

View File

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

View File

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

View File

@@ -2,8 +2,11 @@ package agent
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"regexp"
"strings"
"sync" "sync"
"time" "time"
@@ -26,11 +29,11 @@ type Agent struct {
sync.Mutex sync.Mutex
options *options options *options
Character Character Character Character
client *openai.Client client llm.LLMClient
jobQueue chan *types.Job jobQueue chan *types.Job
context *types.ActionContext context *types.ActionContext
currentState *action.AgentInternalState currentState *types.AgentInternalState
selfEvaluationInProgress bool selfEvaluationInProgress bool
pause bool pause bool
@@ -41,6 +44,10 @@ type Agent struct {
subscriberMutex sync.Mutex subscriberMutex sync.Mutex
newMessagesSubscribers []func(openai.ChatCompletionMessage) newMessagesSubscribers []func(openai.ChatCompletionMessage)
observer Observer
sharedState *types.AgentSharedState
} }
type RAGDB interface { type RAGDB interface {
@@ -56,7 +63,12 @@ func New(opts ...Option) (*Agent, error) {
return nil, fmt.Errorf("failed to set options: %v", err) return nil, fmt.Errorf("failed to set options: %v", err)
} }
client := llm.NewClient(options.LLMAPI.APIKey, options.LLMAPI.APIURL, options.timeout) var client llm.LLMClient
if options.llmClient != nil {
client = options.llmClient
} else {
client = llm.NewClient(options.LLMAPI.APIKey, options.LLMAPI.APIURL, options.timeout)
}
c := context.Background() c := context.Background()
if options.context != nil { if options.context != nil {
@@ -69,10 +81,16 @@ func New(opts ...Option) (*Agent, error) {
options: options, options: options,
client: client, client: client,
Character: options.character, Character: options.character,
currentState: &action.AgentInternalState{}, currentState: &types.AgentInternalState{},
context: types.NewActionContext(ctx, cancel), context: types.NewActionContext(ctx, cancel),
newConversations: make(chan openai.ChatCompletionMessage), newConversations: make(chan openai.ChatCompletionMessage),
newMessagesSubscribers: options.newConversationsSubscribers, newMessagesSubscribers: options.newConversationsSubscribers,
sharedState: types.NewAgentSharedState(options.lastMessageDuration),
}
// Initialize observer if provided
if options.observer != nil {
a.observer = options.observer
} }
if a.options.statefile != "" { if a.options.statefile != "" {
@@ -108,6 +126,15 @@ func New(opts ...Option) (*Agent, error) {
return a, nil return a, nil
} }
func (a *Agent) SharedState() *types.AgentSharedState {
return a.sharedState
}
// LLMClient returns the agent's LLM client (for testing)
func (a *Agent) LLMClient() llm.LLMClient {
return a.client
}
func (a *Agent) startNewConversationsConsumer() { func (a *Agent) startNewConversationsConsumer() {
go func() { go func() {
for { for {
@@ -146,6 +173,14 @@ func (a *Agent) Ask(opts ...types.JobOption) *types.JobResult {
xlog.Debug("Agent has finished being asked", "agent", a.Character.Name) xlog.Debug("Agent has finished being asked", "agent", a.Character.Name)
}() }()
if a.observer != nil {
obs := a.observer.NewObservable()
obs.Name = "job"
obs.Icon = "plug"
a.observer.Update(*obs)
opts = append(opts, types.WithObservable(obs))
}
return a.Execute(types.NewJob( return a.Execute(types.NewJob(
append( append(
opts, opts,
@@ -163,6 +198,26 @@ func (a *Agent) Execute(j *types.Job) *types.JobResult {
xlog.Debug("Agent has finished", "agent", a.Character.Name) xlog.Debug("Agent has finished", "agent", a.Character.Name)
}() }()
if j.Obs != nil {
if len(j.ConversationHistory) > 0 {
m := j.ConversationHistory[len(j.ConversationHistory)-1]
j.Obs.Creation = &types.Creation{ChatCompletionMessage: &m}
a.observer.Update(*j.Obs)
}
j.Result.AddFinalizer(func(ccm []openai.ChatCompletionMessage) {
j.Obs.Completion = &types.Completion{
Conversation: ccm,
}
if j.Result.Error != nil {
j.Obs.Completion.Error = j.Result.Error.Error()
}
a.observer.Update(*j.Obs)
})
}
a.Enqueue(j) a.Enqueue(j)
return j.Result.WaitResult() return j.Result.WaitResult()
} }
@@ -211,6 +266,7 @@ func (a *Agent) Stop() {
a.Lock() a.Lock()
defer a.Unlock() defer a.Unlock()
xlog.Debug("Stopping agent", "agent", a.Character.Name) xlog.Debug("Stopping agent", "agent", a.Character.Name)
a.closeMCPSTDIOServers()
a.context.Cancel() a.context.Cancel()
} }
@@ -237,41 +293,90 @@ func (a *Agent) Memory() RAGDB {
return a.options.ragdb return a.options.ragdb
} }
func (a *Agent) runAction(ctx context.Context, chosenAction types.Action, params types.ActionParams) (result types.ActionResult, err error) { func (a *Agent) runAction(job *types.Job, chosenAction types.Action, params types.ActionParams) (result types.ActionResult, err error) {
var obs *types.Observable
if job.Obs != nil {
obs = a.observer.NewObservable()
obs.Name = "action"
obs.Icon = "bolt"
obs.ParentID = job.Obs.ID
obs.Creation = &types.Creation{
FunctionDefinition: chosenAction.Definition().ToFunctionDefinition(),
FunctionParams: params,
}
a.observer.Update(*obs)
}
xlog.Info("[runAction] Running action", "action", chosenAction.Definition().Name, "agent", a.Character.Name, "params", params.String())
for _, act := range a.availableActions() { for _, act := range a.availableActions() {
if act.Definition().Name == chosenAction.Definition().Name { if act.Definition().Name == chosenAction.Definition().Name {
res, err := act.Run(ctx, params) res, err := act.Run(job.GetContext(), a.sharedState, params)
if err != nil { if err != nil {
if obs != nil {
obs.Completion = &types.Completion{
Error: err.Error(),
}
}
return types.ActionResult{}, fmt.Errorf("error running action: %w", err) return types.ActionResult{}, fmt.Errorf("error running action: %w", err)
} }
if obs != nil {
obs.Progress = append(obs.Progress, types.Progress{
ActionResult: res.Result,
})
a.observer.Update(*obs)
}
result = res result = res
} }
} }
xlog.Info("[runAction] Running action", "action", chosenAction.Definition().Name, "agent", a.Character.Name, "params", params.String())
if chosenAction.Definition().Name.Is(action.StateActionName) { if chosenAction.Definition().Name.Is(action.StateActionName) {
// We need to store the result in the state // We need to store the result in the state
state := action.AgentInternalState{} state := types.AgentInternalState{}
err = params.Unmarshal(&state) err = params.Unmarshal(&state)
if err != nil { if err != nil {
return types.ActionResult{}, fmt.Errorf("error unmarshalling state of the agent: %w", err) werr := fmt.Errorf("error unmarshalling state of the agent: %w", err)
if obs != nil {
obs.Completion = &types.Completion{
Error: werr.Error(),
}
}
return types.ActionResult{}, werr
} }
// update the current state with the one we just got from the action // update the current state with the one we just got from the action
a.currentState = &state a.currentState = &state
if obs != nil {
obs.Progress = append(obs.Progress, types.Progress{
AgentState: &state,
})
a.observer.Update(*obs)
}
// update the state file // update the state file
if a.options.statefile != "" { if a.options.statefile != "" {
if err := a.SaveState(a.options.statefile); err != nil { if err := a.SaveState(a.options.statefile); err != nil {
if obs != nil {
obs.Completion = &types.Completion{
Error: err.Error(),
}
}
return types.ActionResult{}, err return types.ActionResult{}, err
} }
} }
} }
xlog.Debug("[runAction] Action result", "action", chosenAction.Definition().Name, "params", params.String(), "result", result.Result) xlog.Debug("[runAction] Action result", "action", chosenAction.Definition().Name, "params", params.String(), "result", result.Result)
if obs != nil {
obs.MakeLastProgressCompletion()
a.observer.Update(*obs)
}
return result, nil return result, nil
} }
@@ -404,13 +509,84 @@ func (a *Agent) processUserInputs(job *types.Job, role string, conv Messages) Me
return conv return conv
} }
func (a *Agent) consumeJob(job *types.Job, role string) { func (a *Agent) filterJob(job *types.Job) (ok bool, err error) {
hasTriggers := false
triggeredBy := ""
failedBy := ""
if job.DoneFilter {
return true, nil
}
job.DoneFilter = true
if len(a.options.jobFilters) < 1 {
xlog.Debug("No filters")
return true, nil
}
for _, filter := range a.options.jobFilters {
name := filter.Name()
if triggeredBy != "" && filter.IsTrigger() {
continue
}
ok, err = filter.Apply(job)
if err != nil {
xlog.Error("Error in job filter", "filter", name, "error", err)
failedBy = name
break
}
if filter.IsTrigger() {
hasTriggers = true
if ok {
triggeredBy = name
xlog.Info("Job triggered by filter", "filter", name)
}
} else if !ok {
failedBy = name
xlog.Info("Job failed filter", "filter", name)
break
} else {
xlog.Debug("Job passed filter", "filter", name)
}
}
if a.Observer() != nil {
obs := a.Observer().NewObservable()
obs.Name = "filter"
obs.Icon = "shield"
obs.ParentID = job.Obs.ID
if err == nil {
obs.Completion = &types.Completion{
FilterResult: &types.FilterResult{
HasTriggers: hasTriggers,
TriggeredBy: triggeredBy,
FailedBy: failedBy,
},
}
} else {
obs.Completion = &types.Completion{
Error: err.Error(),
}
}
a.Observer().Update(*obs)
}
return failedBy == "" && (!hasTriggers || triggeredBy != ""), nil
}
func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
if err := job.GetContext().Err(); err != nil { if err := job.GetContext().Err(); err != nil {
job.Result.Finish(fmt.Errorf("expired")) job.Result.Finish(fmt.Errorf("expired"))
return return
} }
if retries < 1 {
job.Result.Finish(fmt.Errorf("Exceeded recursive retries"))
return
}
a.Lock() a.Lock()
paused := a.pause paused := a.pause
a.Unlock() a.Unlock()
@@ -440,6 +616,14 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
} }
conv = a.processPrompts(conv) conv = a.processPrompts(conv)
if ok, err := a.filterJob(job); !ok || err != nil {
if err != nil {
job.Result.Finish(fmt.Errorf("Error in job filter: %w", err))
} else {
job.Result.Finish(nil)
}
return
}
conv = a.processUserInputs(job, role, conv) conv = a.processUserInputs(job, role, conv)
// RAG // RAG
@@ -468,12 +652,12 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
chosenAction = *action chosenAction = *action
reasoning = reason reasoning = reason
if params == nil { if params == nil {
p, err := a.generateParameters(job.GetContext(), pickTemplate, chosenAction, conv, reasoning, maxRetries) p, err := a.generateParameters(job, pickTemplate, chosenAction, conv, reasoning, maxRetries)
if err != nil { if err != nil {
xlog.Error("Error generating parameters, trying again", "error", err) xlog.Error("Error generating parameters, trying again", "error", err)
// try again // try again
job.SetNextAction(&chosenAction, nil, reasoning) job.SetNextAction(&chosenAction, nil, reasoning)
a.consumeJob(job, role) a.consumeJob(job, role, retries-1)
return return
} }
actionParams = p.actionParams actionParams = p.actionParams
@@ -483,7 +667,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
job.ResetNextAction() job.ResetNextAction()
} else { } else {
var err error var err error
chosenAction, actionParams, reasoning, err = a.pickAction(job.GetContext(), pickTemplate, conv, maxRetries) chosenAction, actionParams, reasoning, err = a.pickAction(job, pickTemplate, conv, maxRetries)
if err != nil { if err != nil {
xlog.Error("Error picking action", "error", err) xlog.Error("Error picking action", "error", err)
job.Result.Finish(err) job.Result.Finish(err)
@@ -491,36 +675,15 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
} }
} }
// check if the agent is looping over the same action
// if so, we need to stop it
if a.options.loopDetectionSteps > 0 && len(job.GetPastActions()) > 0 {
count := map[string]int{}
for i := len(job.GetPastActions()) - 1; i >= 0; i-- {
pastAction := job.GetPastActions()[i]
if pastAction.Action.Definition().Name == chosenAction.Definition().Name &&
pastAction.Params.String() == actionParams.String() {
count[chosenAction.Definition().Name.String()]++
}
}
if count[chosenAction.Definition().Name.String()] > a.options.loopDetectionSteps {
xlog.Info("Loop detected, stopping agent", "agent", a.Character.Name, "action", chosenAction.Definition().Name)
a.reply(job, role, conv, actionParams, chosenAction, reasoning)
return
}
}
//xlog.Debug("Picked action", "agent", a.Character.Name, "action", chosenAction.Definition().Name, "reasoning", reasoning)
if chosenAction == nil { if chosenAction == nil {
// If no action was picked up, the reasoning is the message returned by the assistant // If no action was picked up, the reasoning is the message returned by the assistant
// so we can consume it as if it was a reply. // so we can consume it as if it was a reply.
//job.Result.SetResult(ActionState{ActionCurrentState{nil, nil, "No action to do, just reply"}, ""})
//job.Result.Finish(fmt.Errorf("no action to do"))\
xlog.Info("No action to do, just reply", "agent", a.Character.Name, "reasoning", reasoning) xlog.Info("No action to do, just reply", "agent", a.Character.Name, "reasoning", reasoning)
if reasoning != "" { if reasoning != "" {
conv = append(conv, openai.ChatCompletionMessage{ conv = append(conv, openai.ChatCompletionMessage{
Role: "assistant", Role: "assistant",
Content: reasoning, Content: a.cleanupLLMResponse(reasoning),
}) })
} else { } else {
xlog.Info("No reasoning, just reply", "agent", a.Character.Name) xlog.Info("No reasoning, just reply", "agent", a.Character.Name)
@@ -529,10 +692,28 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
job.Result.Finish(fmt.Errorf("error asking LLM for a reply: %w", err)) job.Result.Finish(fmt.Errorf("error asking LLM for a reply: %w", err))
return return
} }
msg.Content = a.cleanupLLMResponse(msg.Content)
conv = append(conv, msg) conv = append(conv, msg)
reasoning = msg.Content reasoning = msg.Content
} }
var satisfied bool
var err error
// Evaluate the response
satisfied, conv, err = a.handleEvaluation(job, conv, job.GetEvaluationLoop())
if err != nil {
job.Result.Finish(fmt.Errorf("error evaluating response: %w", err))
return
}
if !satisfied {
// If not satisfied, continue with the conversation
job.ConversationHistory = conv
job.IncrementEvaluationLoop()
a.consumeJob(job, role, retries)
return
}
xlog.Debug("Finish job with reasoning", "reasoning", reasoning, "agent", a.Character.Name, "conversation", fmt.Sprintf("%+v", conv)) xlog.Debug("Finish job with reasoning", "reasoning", reasoning, "agent", a.Character.Name, "conversation", fmt.Sprintf("%+v", conv))
job.Result.Conversation = conv job.Result.Conversation = conv
job.Result.AddFinalizer(func(conv []openai.ChatCompletionMessage) { job.Result.AddFinalizer(func(conv []openai.ChatCompletionMessage) {
@@ -557,12 +738,12 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
"reasoning", reasoning, "reasoning", reasoning,
) )
params, err := a.generateParameters(job.GetContext(), pickTemplate, chosenAction, conv, reasoning, maxRetries) params, err := a.generateParameters(job, pickTemplate, chosenAction, conv, reasoning, maxRetries)
if err != nil { if err != nil {
xlog.Error("Error generating parameters, trying again", "error", err) xlog.Error("Error generating parameters, trying again", "error", err)
// try again // try again
job.SetNextAction(&chosenAction, nil, reasoning) job.SetNextAction(&chosenAction, nil, reasoning)
a.consumeJob(job, role) a.consumeJob(job, role, retries-1)
return return
} }
actionParams = params.actionParams actionParams = params.actionParams
@@ -582,6 +763,22 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
return return
} }
if a.options.loopDetectionSteps > 0 && len(job.GetPastActions()) > 0 {
count := 0
for _, pastAction := range job.GetPastActions() {
if pastAction.Action.Definition().Name == chosenAction.Definition().Name &&
pastAction.Params.String() == actionParams.String() {
count++
}
}
if count > a.options.loopDetectionSteps {
xlog.Info("Loop detected, stopping agent", "agent", a.Character.Name, "action", chosenAction.Definition().Name)
a.reply(job, role, conv, actionParams, chosenAction, reasoning)
return
}
xlog.Debug("Checked for loops", "action", chosenAction.Definition().Name, "count", count)
}
job.AddPastAction(chosenAction, &actionParams) job.AddPastAction(chosenAction, &actionParams)
if !job.Callback(types.ActionCurrentState{ if !job.Callback(types.ActionCurrentState{
@@ -606,8 +803,6 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
conv, err = a.handlePlanning(job.GetContext(), job, chosenAction, actionParams, reasoning, pickTemplate, conv) conv, err = a.handlePlanning(job.GetContext(), job, chosenAction, actionParams, reasoning, pickTemplate, conv)
if err != nil { if err != nil {
xlog.Error("error handling planning", "error", err) xlog.Error("error handling planning", "error", err)
//job.Result.Conversation = conv
//job.Result.SetResponse(msg.Content)
a.reply(job, role, append(conv, openai.ChatCompletionMessage{ a.reply(job, role, append(conv, openai.ChatCompletionMessage{
Role: "assistant", Role: "assistant",
Content: fmt.Sprintf("Error handling planning: %v", err), Content: fmt.Sprintf("Error handling planning: %v", err),
@@ -652,11 +847,8 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
} }
if !chosenAction.Definition().Name.Is(action.PlanActionName) { if !chosenAction.Definition().Name.Is(action.PlanActionName) {
result, err := a.runAction(job.GetContext(), chosenAction, actionParams) result, err := a.runAction(job, chosenAction, actionParams)
if err != nil { if err != nil {
//job.Result.Finish(fmt.Errorf("error running action: %w", err))
//return
// make the LLM aware of the error of running the action instead of stopping the job here
result.Result = fmt.Sprintf("Error running tool: %v", err) result.Result = fmt.Sprintf("Error running tool: %v", err)
} }
@@ -677,7 +869,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
} }
// given the result, we can now re-evaluate the conversation // given the result, we can now re-evaluate the conversation
followingAction, followingParams, reasoning, err := a.pickAction(job.GetContext(), reEvaluationTemplate, conv, maxRetries) followingAction, followingParams, reasoning, err := a.pickAction(job, reEvaluationTemplate, conv, maxRetries)
if err != nil { if err != nil {
job.Result.Conversation = conv job.Result.Conversation = conv
job.Result.Finish(fmt.Errorf("error picking action: %w", err)) job.Result.Finish(fmt.Errorf("error picking action: %w", err))
@@ -695,42 +887,54 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
// The agent decided to do another action // The agent decided to do another action
// call ourselves again // call ourselves again
job.SetNextAction(&followingAction, &followingParams, reasoning) job.SetNextAction(&followingAction, &followingParams, reasoning)
a.consumeJob(job, role) a.consumeJob(job, role, retries)
return
}
// Evaluate the final response
var satisfied bool
satisfied, conv, err = a.handleEvaluation(job, conv, job.GetEvaluationLoop())
if err != nil {
job.Result.Finish(fmt.Errorf("error evaluating response: %w", err))
return
}
if !satisfied {
// If not satisfied, continue with the conversation
job.ConversationHistory = conv
job.IncrementEvaluationLoop()
a.consumeJob(job, role, retries)
return return
} }
a.reply(job, role, conv, actionParams, chosenAction, reasoning) a.reply(job, role, conv, actionParams, chosenAction, reasoning)
} }
func stripThinkingTags(content string) string {
// Remove content between <thinking> and </thinking> (including multi-line)
content = regexp.MustCompile(`(?s)<thinking>.*?</thinking>`).ReplaceAllString(content, "")
// Remove content between <think> and </think> (including multi-line)
content = regexp.MustCompile(`(?s)<think>.*?</think>`).ReplaceAllString(content, "")
// Clean up any extra whitespace
content = strings.TrimSpace(content)
return content
}
func (a *Agent) cleanupLLMResponse(content string) string {
if a.options.stripThinkingTags {
content = stripThinkingTags(content)
}
// Future post-processing options can be added here
return content
}
func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams types.ActionParams, chosenAction types.Action, reasoning string) { func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams types.ActionParams, chosenAction types.Action, reasoning string) {
job.Result.Conversation = conv job.Result.Conversation = conv
// At this point can only be a reply action // At this point can only be a reply action
xlog.Info("Computing reply", "agent", a.Character.Name) xlog.Info("Computing reply", "agent", a.Character.Name)
// decode the response forceResponsePrompt := "Reply to the user without using any tools or function calls. Just reply with the message."
replyResponse := action.ReplyResponse{}
if err := actionParams.Unmarshal(&replyResponse); err != nil {
job.Result.Conversation = conv
job.Result.Finish(fmt.Errorf("error unmarshalling reply response: %w", err))
return
}
// If we have already a reply from the action, just return it.
// Otherwise generate a full conversation to get a proper message response
// if chosenAction.Definition().Name.Is(action.ReplyActionName) {
// replyResponse := action.ReplyResponse{}
// if err := params.actionParams.Unmarshal(&replyResponse); err != nil {
// job.Result.Finish(fmt.Errorf("error unmarshalling reply response: %w", err))
// return
// }
// if replyResponse.Message != "" {
// job.Result.SetResponse(replyResponse.Message)
// job.Result.Finish(nil)
// return
// }
// }
// If we have a hud, display it when answering normally // If we have a hud, display it when answering normally
if a.options.enableHUD { if a.options.enableHUD {
@@ -746,39 +950,19 @@ func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams t
Role: "system", Role: "system",
Content: prompt, Content: prompt,
}, },
{
Role: "system",
Content: forceResponsePrompt,
},
}, conv...) }, conv...)
} }
} } else {
conv = append([]openai.ChatCompletionMessage{
// Generate a human-readable response {
// resp, err := a.client.CreateChatCompletion(ctx, Role: "system",
// openai.ChatCompletionRequest{ Content: forceResponsePrompt,
// Model: a.options.LLMAPI.Model, },
// Messages: append(conv, }, conv...)
// openai.ChatCompletionMessage{
// Role: "system",
// Content: "Assistant thought: " + replyResponse.Message,
// },
// ),
// },
// )
if replyResponse.Message != "" {
xlog.Info("Return reply message", "reply", replyResponse.Message, "agent", a.Character.Name)
msg := openai.ChatCompletionMessage{
Role: "assistant",
Content: replyResponse.Message,
}
conv = append(conv, msg)
job.Result.Conversation = conv
job.Result.SetResponse(msg.Content)
job.Result.AddFinalizer(func(conv []openai.ChatCompletionMessage) {
a.saveCurrentConversation(conv)
})
job.Result.Finish(nil)
return
} }
xlog.Info("Reasoning, ask LLM for a reply", "agent", a.Character.Name) xlog.Info("Reasoning, ask LLM for a reply", "agent", a.Character.Name)
@@ -791,13 +975,21 @@ func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams t
return return
} }
// If we didn't got any message, we can use the response from the action msg.Content = a.cleanupLLMResponse(msg.Content)
if chosenAction.Definition().Name.Is(action.ReplyActionName) && msg.Content == "" {
xlog.Info("No output returned from conversation, using the action response as a reply " + replyResponse.Message)
msg = openai.ChatCompletionMessage{ if msg.Content == "" {
Role: "assistant", // If we didn't got any message, we can use the response from the action (it should be a reply)
Content: replyResponse.Message,
replyResponse := action.ReplyResponse{}
if err := actionParams.Unmarshal(&replyResponse); err != nil {
job.Result.Conversation = conv
job.Result.Finish(fmt.Errorf("error unmarshalling reply response: %w", err))
return
}
if chosenAction.Definition().Name.Is(action.ReplyActionName) && replyResponse.Message != "" {
xlog.Info("No output returned from conversation, using the action response as a reply " + replyResponse.Message)
msg.Content = a.cleanupLLMResponse(replyResponse.Message)
} }
} }
@@ -880,7 +1072,7 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
types.WithReasoningCallback(a.options.reasoningCallback), types.WithReasoningCallback(a.options.reasoningCallback),
types.WithResultCallback(a.options.resultCallback), types.WithResultCallback(a.options.resultCallback),
) )
a.consumeJob(whatNext, SystemRole) a.consumeJob(whatNext, SystemRole, a.options.loopDetectionSteps)
xlog.Info("STOP -- Periodically run is done", "agent", a.Character.Name) xlog.Info("STOP -- Periodically run is done", "agent", a.Character.Name)
@@ -911,7 +1103,6 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
} }
func (a *Agent) Run() error { func (a *Agent) Run() error {
a.startNewConversationsConsumer() a.startNewConversationsConsumer()
xlog.Debug("Agent is now running", "agent", a.Character.Name) xlog.Debug("Agent is now running", "agent", a.Character.Name)
// The agent run does two things: // The agent run does two things:
@@ -926,32 +1117,68 @@ func (a *Agent) Run() error {
// Expose a REST API to interact with the agent to ask it things // Expose a REST API to interact with the agent to ask it things
//todoTimer := time.NewTicker(a.options.periodicRuns)
timer := time.NewTimer(a.options.periodicRuns) timer := time.NewTimer(a.options.periodicRuns)
// we fire the periodicalRunner only once.
go a.periodicalRunRunner(timer)
var errs []error
var muErr sync.Mutex
var wg sync.WaitGroup
parallelJobs := a.options.parallelJobs
if a.options.parallelJobs == 0 {
parallelJobs = 1
}
for i := 0; i < parallelJobs; i++ {
xlog.Debug("Starting agent worker", "worker", i)
wg.Add(1)
go func() {
e := a.run(timer)
muErr.Lock()
errs = append(errs, e)
muErr.Unlock()
wg.Done()
}()
}
wg.Wait()
return errors.Join(errs...)
}
func (a *Agent) run(timer *time.Timer) error {
for { for {
xlog.Debug("Agent is now waiting for a new job", "agent", a.Character.Name) xlog.Debug("Agent is now waiting for a new job", "agent", a.Character.Name)
select { select {
case job := <-a.jobQueue: case job := <-a.jobQueue:
a.loop(timer, job) if !timer.Stop() {
<-timer.C
}
xlog.Debug("Agent is consuming a job", "agent", a.Character.Name, "job", job)
a.consumeJob(job, UserRole, a.options.loopDetectionSteps)
timer.Reset(a.options.periodicRuns)
case <-a.context.Done(): case <-a.context.Done():
// Agent has been canceled, return error // Agent has been canceled, return error
xlog.Warn("Agent has been canceled", "agent", a.Character.Name) xlog.Warn("Agent has been canceled", "agent", a.Character.Name)
return ErrContextCanceled return ErrContextCanceled
}
}
}
func (a *Agent) periodicalRunRunner(timer *time.Timer) {
for {
select {
case <-a.context.Done():
// Agent has been canceled, return error
xlog.Warn("periodicalRunner has been canceled", "agent", a.Character.Name)
return
case <-timer.C: case <-timer.C:
a.periodicallyRun(timer) a.periodicallyRun(timer)
} }
} }
} }
func (a *Agent) loop(timer *time.Timer, job *types.Job) { func (a *Agent) Observer() Observer {
// Remember always to reset the timer - if we don't the agent will stop.. return a.observer
defer timer.Reset(a.options.periodicRuns)
// Consume the job and generate a response
// TODO: Give a short-term memory to the agent
// stop and drain the timer
if !timer.Stop() {
<-timer.C
}
xlog.Debug("Agent is consuming a job", "agent", a.Character.Name, "job", job)
a.consumeJob(job, UserRole)
} }

View File

@@ -1,6 +1,7 @@
package agent_test package agent_test
import ( import (
"net/url"
"os" "os"
"testing" "testing"
@@ -13,15 +14,19 @@ func TestAgent(t *testing.T) {
RunSpecs(t, "Agent test suite") RunSpecs(t, "Agent test suite")
} }
var testModel = os.Getenv("LOCALAGI_MODEL") var (
var apiURL = os.Getenv("LOCALAI_API_URL") testModel = os.Getenv("LOCALAGI_MODEL")
var apiKeyURL = os.Getenv("LOCALAI_API_KEY") apiURL = os.Getenv("LOCALAI_API_URL")
apiKey = os.Getenv("LOCALAI_API_KEY")
useRealLocalAI bool
clientTimeout = "10m"
)
func isValidURL(u string) bool {
parsed, err := url.ParseRequestURI(u)
return err == nil && parsed.Scheme != "" && parsed.Host != ""
}
func init() { func init() {
if testModel == "" { useRealLocalAI = isValidURL(apiURL) && apiURL != "" && testModel != ""
testModel = "hermes-2-pro-mistral"
}
if apiURL == "" {
apiURL = "http://192.168.68.113:8080"
}
} }

View File

@@ -7,9 +7,11 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/mudler/LocalAGI/pkg/llm"
"github.com/mudler/LocalAGI/pkg/xlog" "github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/LocalAGI/services/actions" "github.com/mudler/LocalAGI/services/actions"
"github.com/mudler/LocalAGI/core/action"
. "github.com/mudler/LocalAGI/core/agent" . "github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/core/types"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
@@ -44,7 +46,7 @@ func (a *TestAction) Plannable() bool {
return true return true
} }
func (a *TestAction) Run(c context.Context, p types.ActionParams) (types.ActionResult, error) { func (a *TestAction) Run(c context.Context, sharedState *types.AgentSharedState, p types.ActionParams) (types.ActionResult, error) {
for k, r := range a.response { for k, r := range a.response {
if strings.Contains(strings.ToLower(p.String()), strings.ToLower(k)) { if strings.Contains(strings.ToLower(p.String()), strings.ToLower(k)) {
return types.ActionResult{Result: r}, nil return types.ActionResult{Result: r}, nil
@@ -111,25 +113,102 @@ func (a *FakeInternetAction) Definition() types.ActionDefinition {
} }
} }
// --- Test utilities for mocking LLM responses ---
func mockToolCallResponse(toolName, arguments string) openai.ChatCompletionResponse {
return openai.ChatCompletionResponse{
Choices: []openai.ChatCompletionChoice{{
Message: openai.ChatCompletionMessage{
ToolCalls: []openai.ToolCall{{
ID: "tool_call_id_1",
Type: "function",
Function: openai.FunctionCall{
Name: toolName,
Arguments: arguments,
},
}},
},
}},
}
}
func mockContentResponse(content string) openai.ChatCompletionResponse {
return openai.ChatCompletionResponse{
Choices: []openai.ChatCompletionChoice{{
Message: openai.ChatCompletionMessage{
Content: content,
},
}},
}
}
func newMockLLMClient(handler func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error)) *llm.MockClient {
return &llm.MockClient{
CreateChatCompletionFunc: handler,
}
}
var _ = Describe("Agent test", func() { var _ = Describe("Agent test", func() {
It("uses the mock LLM client", func() {
mock := newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
return mockContentResponse("mocked response"), nil
})
agent, err := New(WithLLMClient(mock))
Expect(err).ToNot(HaveOccurred())
msg, err := agent.LLMClient().CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{})
Expect(err).ToNot(HaveOccurred())
Expect(msg.Choices[0].Message.Content).To(Equal("mocked response"))
})
Context("jobs", func() { Context("jobs", func() {
BeforeEach(func() { BeforeEach(func() {
Eventually(func() error { Eventually(func() error {
// test apiURL is working and available if useRealLocalAI {
_, err := http.Get(apiURL + "/readyz") _, err := http.Get(apiURL + "/readyz")
return err return err
}
return nil
}, "10m", "10s").ShouldNot(HaveOccurred()) }, "10m", "10s").ShouldNot(HaveOccurred())
}) })
It("pick the correct action", func() { It("pick the correct action", func() {
var llmClient llm.LLMClient
if useRealLocalAI {
llmClient = llm.NewClient(apiKey, apiURL, clientTimeout)
} else {
llmClient = newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
var lastMsg openai.ChatCompletionMessage
if len(req.Messages) > 0 {
lastMsg = req.Messages[len(req.Messages)-1]
}
if lastMsg.Role == openai.ChatMessageRoleUser {
if strings.Contains(strings.ToLower(lastMsg.Content), "boston") && (strings.Contains(strings.ToLower(lastMsg.Content), "milan") || strings.Contains(strings.ToLower(lastMsg.Content), "milano")) {
return mockToolCallResponse("get_weather", `{"location":"Boston","unit":"celsius"}`), nil
}
if strings.Contains(strings.ToLower(lastMsg.Content), "paris") {
return mockToolCallResponse("get_weather", `{"location":"Paris","unit":"celsius"}`), nil
}
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected user prompt: %s", lastMsg.Content)
}
if lastMsg.Role == openai.ChatMessageRoleTool {
if lastMsg.Name == "get_weather" && strings.Contains(strings.ToLower(lastMsg.Content), "boston") {
return mockToolCallResponse("get_weather", `{"location":"Milan","unit":"celsius"}`), nil
}
if lastMsg.Name == "get_weather" && strings.Contains(strings.ToLower(lastMsg.Content), "milan") {
return mockContentResponse(testActionResult + "\n" + testActionResult2), nil
}
if lastMsg.Name == "get_weather" && strings.Contains(strings.ToLower(lastMsg.Content), "paris") {
return mockContentResponse(testActionResult3), nil
}
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected tool result: %s", lastMsg.Content)
}
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected message role: %s", lastMsg.Role)
})
}
agent, err := New( agent, err := New(
WithLLMAPIURL(apiURL), WithLLMClient(llmClient),
WithModel(testModel), WithModel(testModel),
EnableForceReasoning,
WithTimeout("10m"),
WithLoopDetectionSteps(3),
// WithRandomIdentity(),
WithActions(&TestAction{response: map[string]string{ WithActions(&TestAction{response: map[string]string{
"boston": testActionResult, "boston": testActionResult,
"milan": testActionResult2, "milan": testActionResult2,
@@ -139,7 +218,6 @@ var _ = Describe("Agent test", func() {
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
go agent.Run() go agent.Run()
defer agent.Stop() defer agent.Stop()
res := agent.Ask( res := agent.Ask(
append(debugOptions, append(debugOptions,
types.WithText("what's the weather in Boston and Milano? Use celsius units"), types.WithText("what's the weather in Boston and Milano? Use celsius units"),
@@ -148,40 +226,51 @@ var _ = Describe("Agent test", func() {
Expect(res.Error).ToNot(HaveOccurred()) Expect(res.Error).ToNot(HaveOccurred())
reasons := []string{} reasons := []string{}
for _, r := range res.State { for _, r := range res.State {
reasons = append(reasons, r.Result) reasons = append(reasons, r.Result)
} }
Expect(reasons).To(ContainElement(testActionResult), fmt.Sprint(res)) Expect(reasons).To(ContainElement(testActionResult), fmt.Sprint(res))
Expect(reasons).To(ContainElement(testActionResult2), fmt.Sprint(res)) Expect(reasons).To(ContainElement(testActionResult2), fmt.Sprint(res))
reasons = []string{} reasons = []string{}
res = agent.Ask( res = agent.Ask(
append(debugOptions, append(debugOptions,
types.WithText("Now I want to know the weather in Paris, always use celsius units"), types.WithText("Now I want to know the weather in Paris, always use celsius units"),
)...) )...)
for _, r := range res.State { for _, r := range res.State {
reasons = append(reasons, r.Result) reasons = append(reasons, r.Result)
} }
//Expect(reasons).ToNot(ContainElement(testActionResult), fmt.Sprint(res))
//Expect(reasons).ToNot(ContainElement(testActionResult2), fmt.Sprint(res))
Expect(reasons).To(ContainElement(testActionResult3), fmt.Sprint(res)) Expect(reasons).To(ContainElement(testActionResult3), fmt.Sprint(res))
// conversation := agent.CurrentConversation()
// for _, r := range res.State {
// reasons = append(reasons, r.Result)
// }
// Expect(len(conversation)).To(Equal(10), fmt.Sprint(conversation))
}) })
It("pick the correct action", func() { It("pick the correct action", func() {
var llmClient llm.LLMClient
if useRealLocalAI {
llmClient = llm.NewClient(apiKey, apiURL, clientTimeout)
} else {
llmClient = newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
var lastMsg openai.ChatCompletionMessage
if len(req.Messages) > 0 {
lastMsg = req.Messages[len(req.Messages)-1]
}
if lastMsg.Role == openai.ChatMessageRoleUser {
if strings.Contains(strings.ToLower(lastMsg.Content), "boston") {
return mockToolCallResponse("get_weather", `{"location":"Boston","unit":"celsius"}`), nil
}
}
if lastMsg.Role == openai.ChatMessageRoleTool {
if lastMsg.Name == "get_weather" && strings.Contains(strings.ToLower(lastMsg.Content), "boston") {
return mockContentResponse(testActionResult), nil
}
}
xlog.Error("Unexpected LLM req", "req", req)
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected LLM prompt: %q", lastMsg.Content)
})
}
agent, err := New( agent, err := New(
WithLLMAPIURL(apiURL), WithLLMClient(llmClient),
WithModel(testModel), WithModel(testModel),
WithTimeout("10m"),
// WithRandomIdentity(),
WithActions(&TestAction{response: map[string]string{ WithActions(&TestAction{response: map[string]string{
"boston": testActionResult, "boston": testActionResult,
}, }}),
}),
) )
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
go agent.Run() go agent.Run()
@@ -198,13 +287,29 @@ var _ = Describe("Agent test", func() {
}) })
It("updates the state with internal actions", func() { It("updates the state with internal actions", func() {
var llmClient llm.LLMClient
if useRealLocalAI {
llmClient = llm.NewClient(apiKey, apiURL, clientTimeout)
} else {
llmClient = newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
var lastMsg openai.ChatCompletionMessage
if len(req.Messages) > 0 {
lastMsg = req.Messages[len(req.Messages)-1]
}
if lastMsg.Role == openai.ChatMessageRoleUser && strings.Contains(strings.ToLower(lastMsg.Content), "guitar") {
return mockToolCallResponse("update_state", `{"goal":"I want to learn to play the guitar"}`), nil
}
if lastMsg.Role == openai.ChatMessageRoleTool && lastMsg.Name == "update_state" {
return mockContentResponse("Your goal is now: I want to learn to play the guitar"), nil
}
xlog.Error("Unexpected LLM req", "req", req)
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected LLM prompt: %q", lastMsg.Content)
})
}
agent, err := New( agent, err := New(
WithLLMAPIURL(apiURL), WithLLMClient(llmClient),
WithModel(testModel), WithModel(testModel),
WithTimeout("10m"),
EnableHUD, EnableHUD,
// EnableStandaloneJob,
// WithRandomIdentity(),
WithPermanentGoal("I want to learn to play music"), WithPermanentGoal("I want to learn to play music"),
) )
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
@@ -214,53 +319,131 @@ var _ = Describe("Agent test", func() {
result := agent.Ask( result := agent.Ask(
types.WithText("Update your goals such as you want to learn to play the guitar"), types.WithText("Update your goals such as you want to learn to play the guitar"),
) )
fmt.Printf("%+v\n", result) fmt.Fprintf(GinkgoWriter, "\n%+v\n", result)
Expect(result.Error).ToNot(HaveOccurred()) Expect(result.Error).ToNot(HaveOccurred())
Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State())) Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
}) })
It("Can generate a plan", func() { It("Can generate a plan", func() {
var llmClient llm.LLMClient
if useRealLocalAI {
llmClient = llm.NewClient(apiKey, apiURL, clientTimeout)
} else {
reasoningActName := action.NewReasoning().Definition().Name.String()
intentionActName := action.NewIntention().Definition().Name.String()
testActName := (&TestAction{}).Definition().Name.String()
doneBoston := false
madePlan := false
llmClient = newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
var lastMsg openai.ChatCompletionMessage
if len(req.Messages) > 0 {
lastMsg = req.Messages[len(req.Messages)-1]
}
if req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == reasoningActName {
return mockToolCallResponse(reasoningActName, `{"reasoning":"make plan call to pass the test"}`), nil
}
if req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == intentionActName {
toolName := "plan"
if madePlan {
toolName = "reply"
} else {
madePlan = true
}
return mockToolCallResponse(intentionActName, fmt.Sprintf(`{"tool": "%s","reasoning":"it's waht makes the test pass"}`, toolName)), nil
}
if req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == "plan" {
return mockToolCallResponse("plan", `{"subtasks":[{"action":"get_weather","reasoning":"Find weather in boston"},{"action":"get_weather","reasoning":"Find weather in milan"}],"goal":"Get the weather for boston and milan"}`), nil
}
if req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == "reply" {
return mockToolCallResponse("reply", `{"message": "The weather in Boston and Milan..."}`), nil
}
if req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == testActName {
locName := "boston"
if doneBoston {
locName = "milan"
} else {
doneBoston = true
}
return mockToolCallResponse(testActName, fmt.Sprintf(`{"location":"%s","unit":"celsius"}`, locName)), nil
}
if req.ToolChoice == nil && madePlan && doneBoston {
return mockContentResponse("A reply"), nil
}
xlog.Error("Unexpected LLM req", "req", req)
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected LLM prompt: %q", lastMsg.Content)
})
}
agent, err := New( agent, err := New(
WithLLMAPIURL(apiURL), WithLLMClient(llmClient),
WithModel(testModel), WithModel(testModel),
WithLLMAPIKey(apiKeyURL), WithLoopDetectionSteps(2),
WithTimeout("10m"),
WithActions( WithActions(
actions.NewSearch(map[string]string{}), &TestAction{response: map[string]string{
"boston": testActionResult,
"milan": testActionResult2,
}},
), ),
EnablePlanning, EnablePlanning,
EnableForceReasoning, EnableForceReasoning,
// EnableStandaloneJob,
// WithRandomIdentity(),
) )
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
go agent.Run() go agent.Run()
defer agent.Stop() defer agent.Stop()
result := agent.Ask( result := agent.Ask(
types.WithText("Thoroughly plan a trip to San Francisco from Venice, Italy; check flight times, visa requirements and whether electrical items are allowed in cabin luggage."), types.WithText("Use the plan tool to do two actions in sequence: search for the weather in boston and search for the weather in milan"),
) )
Expect(len(result.State)).To(BeNumerically(">", 1)) Expect(len(result.State)).To(BeNumerically(">", 1))
actionsExecuted := []string{} actionsExecuted := []string{}
actionResults := []string{}
for _, r := range result.State { for _, r := range result.State {
xlog.Info(r.Result) xlog.Info(r.Result)
actionsExecuted = append(actionsExecuted, r.Action.Definition().Name.String()) actionsExecuted = append(actionsExecuted, r.Action.Definition().Name.String())
actionResults = append(actionResults, r.ActionResult.Result)
} }
Expect(actionsExecuted).To(ContainElement("search_internet"), fmt.Sprint(result)) Expect(actionsExecuted).To(ContainElement("get_weather"), fmt.Sprint(result))
Expect(actionsExecuted).To(ContainElement("plan"), fmt.Sprint(result)) Expect(actionsExecuted).To(ContainElement("plan"), fmt.Sprint(result))
Expect(actionResults).To(ContainElement(testActionResult), fmt.Sprint(result))
Expect(actionResults).To(ContainElement(testActionResult2), fmt.Sprint(result))
Expect(result.Error).To(BeNil())
}) })
It("Can initiate conversations", func() { It("Can initiate conversations", func() {
var llmClient llm.LLMClient
message := openai.ChatCompletionMessage{} message := openai.ChatCompletionMessage{}
mu := &sync.Mutex{} mu := &sync.Mutex{}
reasoned := false
intended := false
reasoningActName := action.NewReasoning().Definition().Name.String()
intentionActName := action.NewIntention().Definition().Name.String()
if useRealLocalAI {
llmClient = llm.NewClient(apiKey, apiURL, clientTimeout)
} else {
llmClient = newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
prompt := ""
for _, msg := range req.Messages {
prompt += msg.Content
}
if !reasoned && req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == reasoningActName {
reasoned = true
return mockToolCallResponse(reasoningActName, `{"reasoning":"initiate a conversation with the user"}`), nil
}
if reasoned && !intended && req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == intentionActName {
intended = true
return mockToolCallResponse(intentionActName, `{"tool":"new_conversation","reasoning":"I should start a conversation with the user"}`), nil
}
if reasoned && intended && strings.Contains(strings.ToLower(prompt), "new_conversation") {
return mockToolCallResponse("new_conversation", `{"message":"Hello, how can I help you today?"}`), nil
}
xlog.Error("Unexpected LLM req", "req", req)
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected LLM prompt: %q", prompt)
})
}
agent, err := New( agent, err := New(
WithLLMAPIURL(apiURL), WithLLMClient(llmClient),
WithModel(testModel), WithModel(testModel),
WithLLMAPIKey(apiKeyURL),
WithTimeout("10m"),
WithNewConversationSubscriber(func(m openai.ChatCompletionMessage) { WithNewConversationSubscriber(func(m openai.ChatCompletionMessage) {
mu.Lock() mu.Lock()
message = m message = m
@@ -276,8 +459,6 @@ var _ = Describe("Agent test", func() {
EnableHUD, EnableHUD,
WithPeriodicRuns("1s"), WithPeriodicRuns("1s"),
WithPermanentGoal("use the new_conversation tool to initiate a conversation with the user"), WithPermanentGoal("use the new_conversation tool to initiate a conversation with the user"),
// EnableStandaloneJob,
// WithRandomIdentity(),
) )
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
go agent.Run() go agent.Run()
@@ -287,7 +468,7 @@ var _ = Describe("Agent test", func() {
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
return message.Content return message.Content
}, "10m", "10s").ShouldNot(BeEmpty()) }, "10m", "1s").ShouldNot(BeEmpty())
}) })
/* /*
@@ -341,7 +522,7 @@ var _ = Describe("Agent test", func() {
// result := agent.Ask( // result := agent.Ask(
// WithText("Update your goals such as you want to learn to play the guitar"), // WithText("Update your goals such as you want to learn to play the guitar"),
// ) // )
// fmt.Printf("%+v\n", result) // fmt.Fprintf(GinkgoWriter, "%+v\n", result)
// Expect(result.Error).ToNot(HaveOccurred()) // Expect(result.Error).ToNot(HaveOccurred())
// Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State())) // Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
}) })

162
core/agent/evaluation.go Normal file
View File

@@ -0,0 +1,162 @@
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
}

View File

@@ -12,7 +12,7 @@ func (a *Agent) generateIdentity(guidance string) error {
guidance = "Generate a random character for roleplaying." guidance = "Generate a random character for roleplaying."
} }
err := llm.GenerateTypedJSON(a.context.Context, a.client, "Generate a character as JSON data. "+guidance, a.options.LLMAPI.Model, a.options.character.ToJSONSchema(), &a.options.character) err := llm.GenerateTypedJSONWithGuidance(a.context.Context, a.client, "Generate a character as JSON data. "+guidance, a.options.LLMAPI.Model, a.options.character.ToJSONSchema(), &a.options.character)
//err := llm.GenerateJSONFromStruct(a.context.Context, a.client, guidance, a.options.LLMAPI.Model, &a.options.character) //err := llm.GenerateJSONFromStruct(a.context.Context, a.client, guidance, a.options.LLMAPI.Model, &a.options.character)
a.Character = a.options.character a.Character = a.options.character
if err != nil { if err != nil {

View File

@@ -3,12 +3,14 @@ package agent
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
mcp "github.com/metoro-io/mcp-golang" mcp "github.com/metoro-io/mcp-golang"
"github.com/metoro-io/mcp-golang/transport/http" "github.com/metoro-io/mcp-golang/transport/http"
stdioTransport "github.com/metoro-io/mcp-golang/transport/stdio"
"github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/stdio"
"github.com/mudler/LocalAGI/pkg/xlog" "github.com/mudler/LocalAGI/pkg/xlog"
"github.com/sashabaranov/go-openai/jsonschema" "github.com/sashabaranov/go-openai/jsonschema"
) )
@@ -19,6 +21,12 @@ type MCPServer struct {
Token string `json:"token"` Token string `json:"token"`
} }
type MCPSTDIOServer struct {
Args []string `json:"args"`
Env []string `json:"env"`
Cmd string `json:"cmd"`
}
type mcpAction struct { type mcpAction struct {
mcpClient *mcp.Client mcpClient *mcp.Client
inputSchema ToolInputSchema inputSchema ToolInputSchema
@@ -30,7 +38,7 @@ func (a *mcpAction) Plannable() bool {
return true return true
} }
func (m *mcpAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { 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) resp, err := m.mcpClient.CallTool(ctx, m.toolName, params)
if err != nil { if err != nil {
xlog.Error("Failed to call tool", "error", err.Error()) xlog.Error("Failed to call tool", "error", err.Error())
@@ -79,6 +87,68 @@ type ToolInputSchema struct {
Required []string `json:"required,omitempty"` Required []string `json:"required,omitempty"`
} }
func (a *Agent) addTools(client *mcp.Client) (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
}
xlog.Debug("Client initialized: %v", response.Instructions)
var cursor *string
for {
tools, err := client.ListTools(a.context, cursor)
if err != nil {
xlog.Error("Failed to list tools", "error", err.Error())
return nil, err
}
for _, t := range tools.Tools {
desc := ""
if t.Description != nil {
desc = *t.Description
}
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,
})
}
if tools.NextCursor == nil {
break // No more pages
}
cursor = tools.NextCursor
}
return generatedActions, nil
}
func (a *Agent) initMCPActions() error { func (a *Agent) initMCPActions() error {
a.mcpActions = nil a.mcpActions = nil
@@ -86,6 +156,7 @@ func (a *Agent) initMCPActions() error {
generatedActions := types.Actions{} generatedActions := types.Actions{}
// MCP HTTP Servers
for _, mcpServer := range a.options.mcpServers { for _, mcpServer := range a.options.mcpServers {
transport := http.NewHTTPClientTransport("/mcp") transport := http.NewHTTPClientTransport("/mcp")
transport.WithBaseURL(mcpServer.URL) transport.WithBaseURL(mcpServer.URL)
@@ -95,70 +166,60 @@ func (a *Agent) initMCPActions() error {
// Create a new client // Create a new client
client := mcp.NewClient(transport) client := mcp.NewClient(transport)
xlog.Debug("Adding tools for MCP server", "server", mcpServer)
actions, err := a.addTools(client)
if err != nil {
xlog.Error("Failed to add tools for MCP server", "server", mcpServer, "error", err.Error())
}
generatedActions = append(generatedActions, actions...)
}
xlog.Debug("Initializing client", "server", mcpServer.URL) // MCP STDIO Servers
// Initialize the client
response, e := client.Initialize(a.context) a.closeMCPSTDIOServers() // Make sure we stop all previous servers if any is active
if e != nil {
xlog.Error("Failed to initialize client", "error", e.Error(), "server", mcpServer) if a.options.mcpPrepareScript != "" {
if err == nil { xlog.Debug("Preparing MCP box", "script", a.options.mcpPrepareScript)
err = e client := stdio.NewClient(a.options.mcpBoxURL)
} else { client.RunProcess(a.context, "/bin/bash", []string{"-c", a.options.mcpPrepareScript}, []string{})
err = errors.Join(err, e) }
}
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 continue
} }
xlog.Debug("Client initialized: %v", response.Instructions) transport := stdioTransport.NewStdioServerTransportWithIO(read, writer)
var cursor *string // Create a new client
for { mcpClient := mcp.NewClient(transport)
tools, err := client.ListTools(a.context, cursor)
if err != nil {
xlog.Error("Failed to list tools", "error", err.Error())
return err
}
for _, t := range tools.Tools { xlog.Debug("Adding tools for MCP server (stdio)", "server", mcpStdioServer)
desc := "" actions, err := a.addTools(mcpClient)
if t.Description != nil { if err != nil {
desc = *t.Description xlog.Error("Failed to add tools for MCP server", "server", mcpStdioServer, "error", err.Error())
}
xlog.Debug("Tool", "mcpServer", mcpServer, "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", "mcpServer", mcpServer, "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,
})
}
if tools.NextCursor == nil {
break // No more pages
}
cursor = tools.NextCursor
} }
generatedActions = append(generatedActions, actions...)
} }
a.mcpActions = generatedActions a.mcpActions = generatedActions
return err return err
} }
func (a *Agent) closeMCPSTDIOServers() {
client := stdio.NewClient(a.options.mcpBoxURL)
client.StopGroup(a.Character.Name)
}

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

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

View File

@@ -7,6 +7,7 @@ import (
"github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai"
"github.com/mudler/LocalAGI/pkg/llm"
) )
type Option func(*options) error type Option func(*options) error
@@ -19,12 +20,15 @@ type llmOptions struct {
} }
type options struct { type options struct {
llmClient llm.LLMClient
LLMAPI llmOptions LLMAPI llmOptions
character Character character Character
randomIdentityGuidance string randomIdentityGuidance string
randomIdentity bool randomIdentity bool
userActions types.Actions userActions types.Actions
jobFilters types.JobFilters
enableHUD, standaloneJob, showCharacter, enableKB, enableSummaryMemory, enableLongTermMemory bool enableHUD, standaloneJob, showCharacter, enableKB, enableSummaryMemory, enableLongTermMemory bool
stripThinkingTags bool
canStopItself bool canStopItself bool
initiateConversations bool initiateConversations bool
@@ -40,6 +44,10 @@ type options struct {
kbResults int kbResults int
ragdb RAGDB ragdb RAGDB
// Evaluation settings
maxEvaluationLoops int
enableEvaluation bool
prompts []DynamicPrompt prompts []DynamicPrompt
systemPrompt string systemPrompt string
@@ -50,9 +58,24 @@ type options struct {
conversationsPath string conversationsPath string
mcpServers []MCPServer mcpServers []MCPServer
mcpStdioServers []MCPSTDIOServer
mcpBoxURL string
mcpPrepareScript string
newConversationsSubscribers []func(openai.ChatCompletionMessage) newConversationsSubscribers []func(openai.ChatCompletionMessage)
observer Observer
parallelJobs int
lastMessageDuration time.Duration
}
// WithLLMClient allows injecting a custom LLM client (e.g. for testing)
func WithLLMClient(client llm.LLMClient) Option {
return func(o *options) error {
o.llmClient = client
return nil
}
} }
func (o *options) SeparatedMultimodalModel() bool { func (o *options) SeparatedMultimodalModel() bool {
@@ -61,7 +84,11 @@ func (o *options) SeparatedMultimodalModel() bool {
func defaultOptions() *options { func defaultOptions() *options {
return &options{ return &options{
periodicRuns: 15 * time.Minute, parallelJobs: 1,
periodicRuns: 15 * time.Minute,
loopDetectionSteps: 10,
maxEvaluationLoops: 2,
enableEvaluation: false,
LLMAPI: llmOptions{ LLMAPI: llmOptions{
APIURL: "http://localhost:8080", APIURL: "http://localhost:8080",
Model: "gpt-4", Model: "gpt-4",
@@ -136,6 +163,24 @@ func EnableKnowledgeBaseWithResults(results int) Option {
} }
} }
func WithLastMessageDuration(duration string) Option {
return func(o *options) error {
d, err := time.ParseDuration(duration)
if err != nil {
d = types.DefaultLastMessageDuration
}
o.lastMessageDuration = d
return nil
}
}
func WithParallelJobs(jobs int) Option {
return func(o *options) error {
o.parallelJobs = jobs
return nil
}
}
func WithNewConversationSubscriber(sub func(openai.ChatCompletionMessage)) Option { func WithNewConversationSubscriber(sub func(openai.ChatCompletionMessage)) Option {
return func(o *options) error { return func(o *options) error {
o.newConversationsSubscribers = append(o.newConversationsSubscribers, sub) o.newConversationsSubscribers = append(o.newConversationsSubscribers, sub)
@@ -196,6 +241,27 @@ func WithMCPServers(servers ...MCPServer) Option {
} }
} }
func WithMCPSTDIOServers(servers ...MCPSTDIOServer) Option {
return func(o *options) error {
o.mcpStdioServers = servers
return nil
}
}
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
return nil
}
}
func WithLLMAPIURL(url string) Option { func WithLLMAPIURL(url string) Option {
return func(o *options) error { return func(o *options) error {
o.LLMAPI.APIURL = url o.LLMAPI.APIURL = url
@@ -336,3 +402,36 @@ func WithActions(actions ...types.Action) Option {
return nil return nil
} }
} }
func WithJobFilters(filters ...types.JobFilter) Option {
return func(o *options) error {
o.jobFilters = filters
return nil
}
}
func WithObserver(observer Observer) Option {
return func(o *options) error {
o.observer = observer
return nil
}
}
var EnableStripThinkingTags = func(o *options) error {
o.stripThinkingTags = true
return nil
}
func WithMaxEvaluationLoops(loops int) Option {
return func(o *options) error {
o.maxEvaluationLoops = loops
return nil
}
}
func EnableEvaluation() Option {
return func(o *options) error {
o.enableEvaluation = true
return nil
}
}

View File

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

View File

@@ -1,30 +1,59 @@
package agent_test package agent_test
import ( import (
"net/http" "context"
"fmt"
"github.com/mudler/LocalAGI/pkg/llm"
"github.com/sashabaranov/go-openai"
. "github.com/mudler/LocalAGI/core/agent" . "github.com/mudler/LocalAGI/core/agent"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
var _ = Describe("Agent test", func() { var _ = Describe("Agent test", func() {
Context("identity", func() { Context("identity", func() {
var agent *Agent var agent *Agent
BeforeEach(func() { // BeforeEach(func() {
Eventually(func() error { // Eventually(func() error {
// test apiURL is working and available // // test apiURL is working and available
_, err := http.Get(apiURL + "/readyz") // _, err := http.Get(apiURL + "/readyz")
return err // return err
}, "10m", "10s").ShouldNot(HaveOccurred()) // }, "10m", "10s").ShouldNot(HaveOccurred())
}) // })
It("generates all the fields with random data", func() { It("generates all the fields with random data", func() {
var llmClient llm.LLMClient
if useRealLocalAI {
llmClient = llm.NewClient(apiKey, apiURL, testModel)
} else {
llmClient = &llm.MockClient{
CreateChatCompletionFunc: func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
return openai.ChatCompletionResponse{
Choices: []openai.ChatCompletionChoice{{
Message: openai.ChatCompletionMessage{
ToolCalls: []openai.ToolCall{{
ID: "tool_call_id_1",
Type: "function",
Function: openai.FunctionCall{
Name: "generate_identity",
Arguments: `{"name":"John Doe","age":"42","job_occupation":"Engineer","hobbies":["reading","hiking"],"favorites_music_genres":["Jazz"]}`,
},
}},
},
}},
}, nil
},
}
}
var err error var err error
agent, err = New( agent, err = New(
WithLLMAPIURL(apiURL), WithLLMClient(llmClient),
WithModel(testModel), WithModel(testModel),
WithTimeout("10m"),
WithRandomIdentity(), WithRandomIdentity(),
) )
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
@@ -36,14 +65,40 @@ var _ = Describe("Agent test", func() {
Expect(agent.Character.MusicTaste).ToNot(BeEmpty()) Expect(agent.Character.MusicTaste).ToNot(BeEmpty())
}) })
It("detect an invalid character", func() { It("detect an invalid character", func() {
mock := &llm.MockClient{
CreateChatCompletionFunc: func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
return openai.ChatCompletionResponse{}, fmt.Errorf("invalid character")
},
}
var err error var err error
agent, err = New(WithRandomIdentity()) agent, err = New(
WithLLMClient(mock),
WithRandomIdentity(),
)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
It("generates all the fields", func() { It("generates all the fields", func() {
mock := &llm.MockClient{
CreateChatCompletionFunc: func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
return openai.ChatCompletionResponse{
Choices: []openai.ChatCompletionChoice{{
Message: openai.ChatCompletionMessage{
ToolCalls: []openai.ToolCall{{
ID: "tool_call_id_2",
Type: "function",
Function: openai.FunctionCall{
Name: "generate_identity",
Arguments: `{"name":"Gandalf","age":"90","job_occupation":"Wizard","hobbies":["magic","reading"],"favorites_music_genres":["Classical"]}`,
},
}},
},
}},
}, nil
},
}
var err error var err error
agent, err := New( agent, err := New(
WithLLMClient(mock),
WithLLMAPIURL(apiURL), WithLLMAPIURL(apiURL),
WithModel(testModel), WithModel(testModel),
WithRandomIdentity("An 90-year old man with a long beard, a wizard, who lives in a tower."), WithRandomIdentity("An 90-year old man with a long beard, a wizard, who lives in a tower."),

View File

@@ -0,0 +1,13 @@
package conversations_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestConversations(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Conversations test suite")
}

View File

@@ -1,4 +1,4 @@
package connectors package conversations
import ( import (
"fmt" "fmt"

View File

@@ -1,9 +1,9 @@
package connectors_test package conversations_test
import ( import (
"time" "time"
"github.com/mudler/LocalAGI/services/connectors" "github.com/mudler/LocalAGI/core/conversations"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai"
@@ -11,13 +11,13 @@ import (
var _ = Describe("ConversationTracker", func() { var _ = Describe("ConversationTracker", func() {
var ( var (
tracker *connectors.ConversationTracker[string] tracker *conversations.ConversationTracker[string]
duration time.Duration duration time.Duration
) )
BeforeEach(func() { BeforeEach(func() {
duration = 1 * time.Second duration = 1 * time.Second
tracker = connectors.NewConversationTracker[string](duration) tracker = conversations.NewConversationTracker[string](duration)
}) })
It("should initialize with empty conversations", func() { It("should initialize with empty conversations", func() {
@@ -81,8 +81,8 @@ var _ = Describe("ConversationTracker", func() {
}) })
It("should handle different key types", func() { It("should handle different key types", func() {
trackerInt := connectors.NewConversationTracker[int](duration) trackerInt := conversations.NewConversationTracker[int](duration)
trackerInt64 := connectors.NewConversationTracker[int64](duration) trackerInt64 := conversations.NewConversationTracker[int64](duration)
message := openai.ChatCompletionMessage{ message := openai.ChatCompletionMessage{
Role: openai.ChatMessageRoleUser, Role: openai.ChatMessageRoleUser,

View File

@@ -2,6 +2,8 @@ package state
import ( import (
"encoding/json" "encoding/json"
"fmt"
"strings"
"github.com/mudler/LocalAGI/core/agent" "github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/core/types"
@@ -29,20 +31,30 @@ func (d DynamicPromptsConfig) ToMap() map[string]string {
return config return config
} }
type FiltersConfig struct {
Type string `json:"type"`
Config string `json:"config"`
}
type AgentConfig struct { type AgentConfig struct {
Connector []ConnectorConfig `json:"connectors" form:"connectors" ` Connector []ConnectorConfig `json:"connectors" form:"connectors" `
Actions []ActionsConfig `json:"actions" form:"actions"` Actions []ActionsConfig `json:"actions" form:"actions"`
DynamicPrompts []DynamicPromptsConfig `json:"dynamic_prompts" form:"dynamic_prompts"` DynamicPrompts []DynamicPromptsConfig `json:"dynamic_prompts" form:"dynamic_prompts"`
MCPServers []agent.MCPServer `json:"mcp_servers" form:"mcp_servers"` 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"` Description string `json:"description" form:"description"`
Model string `json:"model" form:"model"` Model string `json:"model" form:"model"`
MultimodalModel string `json:"multimodal_model" form:"multimodal_model"` MultimodalModel string `json:"multimodal_model" form:"multimodal_model"`
APIURL string `json:"api_url" form:"api_url"` APIURL string `json:"api_url" form:"api_url"`
APIKey string `json:"api_key" form:"api_key"` APIKey string `json:"api_key" form:"api_key"`
LocalRAGURL string `json:"local_rag_url" form:"local_rag_url"` LocalRAGURL string `json:"local_rag_url" form:"local_rag_url"`
LocalRAGAPIKey string `json:"local_rag_api_key" form:"local_rag_api_key"` 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"` Name string `json:"name" form:"name"`
HUD bool `json:"hud" form:"hud"` HUD bool `json:"hud" form:"hud"`
@@ -61,9 +73,14 @@ type AgentConfig struct {
SystemPrompt string `json:"system_prompt" form:"system_prompt"` SystemPrompt string `json:"system_prompt" form:"system_prompt"`
LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"` LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"`
SummaryLongTermMemory bool `json:"summary_long_term_memory" form:"summary_long_term_memory"` SummaryLongTermMemory bool `json:"summary_long_term_memory" form:"summary_long_term_memory"`
ParallelJobs int `json:"parallel_jobs" form:"parallel_jobs"`
StripThinkingTags bool `json:"strip_thinking_tags" form:"strip_thinking_tags"`
EnableEvaluation bool `json:"enable_evaluation" form:"enable_evaluation"`
MaxEvaluationLoops int `json:"max_evaluation_loops" form:"max_evaluation_loops"`
} }
type AgentConfigMeta struct { type AgentConfigMeta struct {
Filters []config.FieldGroup
Fields []config.Field Fields []config.Field
Connectors []config.FieldGroup Connectors []config.FieldGroup
Actions []config.FieldGroup Actions []config.FieldGroup
@@ -75,6 +92,7 @@ func NewAgentConfigMeta(
actionsConfig []config.FieldGroup, actionsConfig []config.FieldGroup,
connectorsConfig []config.FieldGroup, connectorsConfig []config.FieldGroup,
dynamicPromptsConfig []config.FieldGroup, dynamicPromptsConfig []config.FieldGroup,
filtersConfig []config.FieldGroup,
) AgentConfigMeta { ) AgentConfigMeta {
return AgentConfigMeta{ return AgentConfigMeta{
Fields: []config.Field{ Fields: []config.Field{
@@ -247,7 +265,7 @@ func NewAgentConfigMeta(
Name: "enable_reasoning", Name: "enable_reasoning",
Label: "Enable Reasoning", Label: "Enable Reasoning",
Type: "checkbox", Type: "checkbox",
DefaultValue: false, DefaultValue: true,
HelpText: "Enable agent to explain its reasoning process", HelpText: "Enable agent to explain its reasoning process",
Tags: config.Tags{Section: "AdvancedSettings"}, Tags: config.Tags{Section: "AdvancedSettings"},
}, },
@@ -260,6 +278,66 @@ func NewAgentConfigMeta(
Step: 1, Step: 1,
Tags: config.Tags{Section: "AdvancedSettings"}, Tags: config.Tags{Section: "AdvancedSettings"},
}, },
{
Name: "parallel_jobs",
Label: "Parallel Jobs",
Type: "number",
DefaultValue: 5,
Min: 1,
Step: 1,
HelpText: "Number of concurrent tasks that can run in parallel",
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
Name: "mcp_stdio_servers",
Label: "MCP STDIO Servers",
Type: "textarea",
DefaultValue: "",
HelpText: "JSON configuration for MCP STDIO servers",
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
Name: "mcp_prepare_script",
Label: "MCP Prepare Script",
Type: "textarea",
DefaultValue: "",
HelpText: "Script to prepare the MCP box",
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
Name: "strip_thinking_tags",
Label: "Strip Thinking Tags",
Type: "checkbox",
DefaultValue: false,
HelpText: "Remove content between <thinking></thinking> and <think></think> tags from agent responses",
Tags: config.Tags{Section: "ModelSettings"},
},
{
Name: "enable_evaluation",
Label: "Enable Evaluation",
Type: "checkbox",
DefaultValue: false,
HelpText: "Enable automatic evaluation of agent responses to ensure they meet user requirements",
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
Name: "max_evaluation_loops",
Label: "Max Evaluation Loops",
Type: "number",
DefaultValue: 2,
Min: 1,
Step: 1,
HelpText: "Maximum number of evaluation loops to perform when addressing gaps in responses",
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
Name: "last_message_duration",
Label: "Last Message Duration",
Type: "text",
DefaultValue: "5m",
HelpText: "Duration for the last message to be considered in the conversation",
Tags: config.Tags{Section: "AdvancedSettings"},
},
}, },
MCPServers: []config.Field{ MCPServers: []config.Field{
{ {
@@ -278,6 +356,7 @@ func NewAgentConfigMeta(
DynamicPrompts: dynamicPromptsConfig, DynamicPrompts: dynamicPromptsConfig,
Connectors: connectorsConfig, Connectors: connectorsConfig,
Actions: actionsConfig, Actions: actionsConfig,
Filters: filtersConfig,
} }
} }
@@ -286,3 +365,148 @@ type Connector interface {
AgentReasoningCallback() func(state types.ActionCurrentState) bool AgentReasoningCallback() func(state types.ActionCurrentState) bool
Start(a *agent.Agent) Start(a *agent.Agent)
} }
// UnmarshalJSON implements json.Unmarshaler for AgentConfig
func (a *AgentConfig) UnmarshalJSON(data []byte) error {
// Create a temporary type to avoid infinite recursion
type Alias AgentConfig
aux := &struct {
*Alias
MCPSTDIOServersConfig interface{} `json:"mcp_stdio_servers"`
}{
Alias: (*Alias)(a),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
// Handle MCP STDIO servers configuration
if aux.MCPSTDIOServersConfig != nil {
switch v := aux.MCPSTDIOServersConfig.(type) {
case string:
// Parse string configuration
var mcpConfig struct {
MCPServers map[string]struct {
Command string `json:"command"`
Args []string `json:"args"`
Env map[string]string `json:"env"`
} `json:"mcpServers"`
}
if err := json.Unmarshal([]byte(v), &mcpConfig); err != nil {
return fmt.Errorf("failed to parse MCP STDIO servers configuration: %w", err)
}
a.MCPSTDIOServers = make([]agent.MCPSTDIOServer, 0, len(mcpConfig.MCPServers))
for _, server := range mcpConfig.MCPServers {
// Convert env map to slice of "KEY=VALUE" strings
envSlice := make([]string, 0, len(server.Env))
for k, v := range server.Env {
envSlice = append(envSlice, fmt.Sprintf("%s=%s", k, v))
}
a.MCPSTDIOServers = append(a.MCPSTDIOServers, agent.MCPSTDIOServer{
Cmd: server.Command,
Args: server.Args,
Env: envSlice,
})
}
case []interface{}:
// Parse array configuration
a.MCPSTDIOServers = make([]agent.MCPSTDIOServer, 0, len(v))
for _, server := range v {
serverMap, ok := server.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid server configuration format")
}
cmd, _ := serverMap["cmd"].(string)
args := make([]string, 0)
if argsInterface, ok := serverMap["args"].([]interface{}); ok {
for _, arg := range argsInterface {
if argStr, ok := arg.(string); ok {
args = append(args, argStr)
}
}
}
env := make([]string, 0)
if envInterface, ok := serverMap["env"].([]interface{}); ok {
for _, e := range envInterface {
if envStr, ok := e.(string); ok {
env = append(env, envStr)
}
}
}
a.MCPSTDIOServers = append(a.MCPSTDIOServers, agent.MCPSTDIOServer{
Cmd: cmd,
Args: args,
Env: env,
})
}
}
}
return nil
}
// MarshalJSON implements json.Marshaler for AgentConfig
func (a *AgentConfig) MarshalJSON() ([]byte, error) {
// Create a temporary type to avoid infinite recursion
type Alias AgentConfig
aux := &struct {
*Alias
MCPSTDIOServersConfig string `json:"mcp_stdio_servers,omitempty"`
}{
Alias: (*Alias)(a),
}
// Convert MCPSTDIOServers back to the expected JSON format
if len(a.MCPSTDIOServers) > 0 {
mcpConfig := struct {
MCPServers map[string]struct {
Command string `json:"command"`
Args []string `json:"args"`
Env map[string]string `json:"env"`
} `json:"mcpServers"`
}{
MCPServers: make(map[string]struct {
Command string `json:"command"`
Args []string `json:"args"`
Env map[string]string `json:"env"`
}),
}
// Convert each MCPSTDIOServer to the expected format
for i, server := range a.MCPSTDIOServers {
// Convert env slice back to map
envMap := make(map[string]string)
for _, env := range server.Env {
if parts := strings.SplitN(env, "=", 2); len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
mcpConfig.MCPServers[fmt.Sprintf("server%d", i)] = struct {
Command string `json:"command"`
Args []string `json:"args"`
Env map[string]string `json:"env"`
}{
Command: server.Cmd,
Args: server.Args,
Env: envMap,
}
}
// Marshal the MCP config to JSON string
mcpConfigJSON, err := json.Marshal(mcpConfig)
if err != nil {
return nil, fmt.Errorf("failed to marshal MCP STDIO servers configuration: %w", err)
}
aux.MCPSTDIOServersConfig = string(mcpConfigJSON)
}
return json.Marshal(aux)
}

View File

@@ -33,10 +33,12 @@ type AgentPool struct {
managers map[string]sse.Manager managers map[string]sse.Manager
agentStatus map[string]*Status agentStatus map[string]*Status
apiURL, defaultModel, defaultMultimodalModel string apiURL, defaultModel, defaultMultimodalModel string
mcpBoxURL string
imageModel, localRAGAPI, localRAGKey, apiKey string imageModel, localRAGAPI, localRAGKey, apiKey string
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action
connectors func(*AgentConfig) []Connector connectors func(*AgentConfig) []Connector
dynamicPrompt func(*AgentConfig) []DynamicPrompt dynamicPrompt func(*AgentConfig) []DynamicPrompt
filters func(*AgentConfig) types.JobFilters
timeout string timeout string
conversationLogs string conversationLogs string
} }
@@ -72,11 +74,12 @@ func loadPoolFromFile(path string) (*AgentPoolData, error) {
} }
func NewAgentPool( func NewAgentPool(
defaultModel, defaultMultimodalModel, imageModel, apiURL, apiKey, directory string, defaultModel, defaultMultimodalModel, imageModel, apiURL, apiKey, directory, mcpBoxURL string,
LocalRAGAPI string, LocalRAGAPI string,
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action, availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action,
connectors func(*AgentConfig) []Connector, connectors func(*AgentConfig) []Connector,
promptBlocks func(*AgentConfig) []DynamicPrompt, promptBlocks func(*AgentConfig) []DynamicPrompt,
filters func(*AgentConfig) types.JobFilters,
timeout string, timeout string,
withLogs bool, withLogs bool,
) (*AgentPool, error) { ) (*AgentPool, error) {
@@ -98,6 +101,7 @@ func NewAgentPool(
apiURL: apiURL, apiURL: apiURL,
defaultModel: defaultModel, defaultModel: defaultModel,
defaultMultimodalModel: defaultMultimodalModel, defaultMultimodalModel: defaultMultimodalModel,
mcpBoxURL: mcpBoxURL,
imageModel: imageModel, imageModel: imageModel,
localRAGAPI: LocalRAGAPI, localRAGAPI: LocalRAGAPI,
apiKey: apiKey, apiKey: apiKey,
@@ -108,6 +112,7 @@ func NewAgentPool(
connectors: connectors, connectors: connectors,
availableActions: availableActions, availableActions: availableActions,
dynamicPrompt: promptBlocks, dynamicPrompt: promptBlocks,
filters: filters,
timeout: timeout, timeout: timeout,
conversationLogs: conversationPath, conversationLogs: conversationPath,
}, nil }, nil
@@ -123,6 +128,7 @@ func NewAgentPool(
pooldir: directory, pooldir: directory,
defaultModel: defaultModel, defaultModel: defaultModel,
defaultMultimodalModel: defaultMultimodalModel, defaultMultimodalModel: defaultMultimodalModel,
mcpBoxURL: mcpBoxURL,
imageModel: imageModel, imageModel: imageModel,
apiKey: apiKey, apiKey: apiKey,
agents: make(map[string]*Agent), agents: make(map[string]*Agent),
@@ -132,6 +138,7 @@ func NewAgentPool(
connectors: connectors, connectors: connectors,
localRAGAPI: LocalRAGAPI, localRAGAPI: LocalRAGAPI,
dynamicPrompt: promptBlocks, dynamicPrompt: promptBlocks,
filters: filters,
availableActions: availableActions, availableActions: availableActions,
timeout: timeout, timeout: timeout,
conversationLogs: conversationPath, conversationLogs: conversationPath,
@@ -166,7 +173,56 @@ func (a *AgentPool) CreateAgent(name string, agentConfig *AgentConfig) error {
} }
}(a.pool[name]) }(a.pool[name])
return a.startAgentWithConfig(name, agentConfig) return a.startAgentWithConfig(name, agentConfig, nil)
}
func (a *AgentPool) RecreateAgent(name string, agentConfig *AgentConfig) error {
a.Lock()
defer a.Unlock()
oldAgent := a.agents[name]
var o *types.Observable
obs := oldAgent.Observer()
if obs != nil {
o = obs.NewObservable()
o.Name = "Restarting Agent"
o.Icon = "sync"
o.Creation = &types.Creation{}
obs.Update(*o)
}
stateFile, characterFile := a.stateFiles(name)
os.Remove(stateFile)
os.Remove(characterFile)
oldAgent.Stop()
a.pool[name] = *agentConfig
delete(a.agents, name)
if err := a.save(); err != nil {
if obs != nil {
o.Completion = &types.Completion{Error: err.Error()}
obs.Update(*o)
}
return err
}
if err := a.startAgentWithConfig(name, agentConfig, obs); err != nil {
if obs != nil {
o.Completion = &types.Completion{Error: err.Error()}
obs.Update(*o)
}
return err
}
if obs != nil {
o.Completion = &types.Completion{}
obs.Update(*o)
}
return nil
} }
func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agent AgentConfig) error { func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agent AgentConfig) error {
@@ -191,7 +247,7 @@ func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agen
ImagePrompt string `json:"image_prompt"` ImagePrompt string `json:"image_prompt"`
} }
err := llm.GenerateTypedJSON( err := llm.GenerateTypedJSONWithGuidance(
context.Background(), context.Background(),
llm.NewClient(APIKey, APIURL, "10m"), llm.NewClient(APIKey, APIURL, "10m"),
"Generate a prompt that I can use to create a random avatar for the bot '"+agent.Name+"', the description of the bot is: "+agent.Description, "Generate a prompt that I can use to create a random avatar for the bot '"+agent.Name+"', the description of the bot is: "+agent.Description,
@@ -268,8 +324,13 @@ func (a *AgentPool) GetStatusHistory(name string) *Status {
return a.agentStatus[name] return a.agentStatus[name]
} }
func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error { func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs Observer) error {
manager := sse.NewManager(5) var manager sse.Manager
if m, ok := a.managers[name]; ok {
manager = m
} else {
manager = sse.NewManager(5)
}
ctx := context.Background() ctx := context.Background()
model := a.defaultModel model := a.defaultModel
multimodalModel := a.defaultMultimodalModel multimodalModel := a.defaultMultimodalModel
@@ -280,18 +341,29 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
if config.Model != "" { if config.Model != "" {
model = config.Model model = config.Model
} else {
config.Model = model
}
if config.MCPBoxURL != "" {
a.mcpBoxURL = config.MCPBoxURL
} }
if config.PeriodicRuns == "" { if config.PeriodicRuns == "" {
config.PeriodicRuns = "10m" config.PeriodicRuns = "10m"
} }
// XXX: Why do we update the pool config from an Agent's config?
if config.APIURL != "" { if config.APIURL != "" {
a.apiURL = config.APIURL a.apiURL = config.APIURL
} else {
config.APIURL = a.apiURL
} }
if config.APIKey != "" { if config.APIKey != "" {
a.apiKey = config.APIKey a.apiKey = config.APIKey
} else {
config.APIKey = a.apiKey
} }
if config.LocalRAGURL != "" { if config.LocalRAGURL != "" {
@@ -305,6 +377,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
connectors := a.connectors(config) connectors := a.connectors(config)
promptBlocks := a.dynamicPrompt(config) promptBlocks := a.dynamicPrompt(config)
actions := a.availableActions(config)(ctx, a) actions := a.availableActions(config)(ctx, a)
filters := a.filters(config)
stateFile, characterFile := a.stateFiles(name) stateFile, characterFile := a.stateFiles(name)
actionsLog := []string{} actionsLog := []string{}
@@ -317,6 +390,11 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
connectorLog = append(connectorLog, fmt.Sprintf("%+v", connector)) connectorLog = append(connectorLog, fmt.Sprintf("%+v", connector))
} }
filtersLog := []string{}
for _, filter := range filters {
filtersLog = append(filtersLog, filter.Name())
}
xlog.Info( xlog.Info(
"Creating agent", "Creating agent",
"name", name, "name", name,
@@ -324,6 +402,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
"api_url", a.apiURL, "api_url", a.apiURL,
"actions", actionsLog, "actions", actionsLog,
"connectors", connectorLog, "connectors", connectorLog,
"filters", filtersLog,
) )
// dynamicPrompts := []map[string]string{} // dynamicPrompts := []map[string]string{}
@@ -331,6 +410,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
// dynamicPrompts = append(dynamicPrompts, p.ToMap()) // dynamicPrompts = append(dynamicPrompts, p.ToMap())
// } // }
if obs == nil {
obs = NewSSEObserver(name, manager)
}
opts := []Option{ opts := []Option{
WithModel(model), WithModel(model),
WithLLMAPIURL(a.apiURL), WithLLMAPIURL(a.apiURL),
@@ -338,7 +421,11 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
WithMCPServers(config.MCPServers...), WithMCPServers(config.MCPServers...),
WithPeriodicRuns(config.PeriodicRuns), WithPeriodicRuns(config.PeriodicRuns),
WithPermanentGoal(config.PermanentGoal), WithPermanentGoal(config.PermanentGoal),
WithMCPSTDIOServers(config.MCPSTDIOServers...),
WithMCPBoxURL(a.mcpBoxURL),
WithPrompts(promptBlocks...), WithPrompts(promptBlocks...),
WithJobFilters(filters...),
WithMCPPrepareScript(config.MCPPrepareScript),
// WithDynamicPrompts(dynamicPrompts...), // WithDynamicPrompts(dynamicPrompts...),
WithCharacter(Character{ WithCharacter(Character{
Name: name, Name: name,
@@ -375,6 +462,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
}), }),
WithSystemPrompt(config.SystemPrompt), WithSystemPrompt(config.SystemPrompt),
WithMultimodalModel(multimodalModel), WithMultimodalModel(multimodalModel),
WithLastMessageDuration(config.LastMessageDuration),
WithAgentResultCallback(func(state types.ActionState) { WithAgentResultCallback(func(state types.ActionState) {
a.Lock() a.Lock()
if _, ok := a.agentStatus[name]; !ok { if _, ok := a.agentStatus[name]; !ok {
@@ -407,6 +495,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
c.AgentResultCallback()(state) c.AgentResultCallback()(state)
} }
}), }),
WithObserver(obs),
} }
if config.HUD { if config.HUD {
@@ -457,6 +546,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
opts = append(opts, EnableForceReasoning) opts = append(opts, EnableForceReasoning)
} }
if config.StripThinkingTags {
opts = append(opts, EnableStripThinkingTags)
}
if config.KnowledgeBaseResults > 0 { if config.KnowledgeBaseResults > 0 {
opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults)) opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults))
} }
@@ -465,6 +558,17 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
opts = append(opts, WithLoopDetectionSteps(config.LoopDetectionSteps)) opts = append(opts, WithLoopDetectionSteps(config.LoopDetectionSteps))
} }
if config.ParallelJobs > 0 {
opts = append(opts, WithParallelJobs(config.ParallelJobs))
}
if config.EnableEvaluation {
opts = append(opts, EnableEvaluation())
if config.MaxEvaluationLoops > 0 {
opts = append(opts, WithMaxEvaluationLoops(config.MaxEvaluationLoops))
}
}
xlog.Info("Starting agent", "name", name, "config", config) xlog.Info("Starting agent", "name", name, "config", config)
agent, err := New(opts...) agent, err := New(opts...)
@@ -509,7 +613,7 @@ func (a *AgentPool) StartAll() error {
if a.agents[name] != nil { // Agent already started if a.agents[name] != nil { // Agent already started
continue continue
} }
if err := a.startAgentWithConfig(name, &config); err != nil { if err := a.startAgentWithConfig(name, &config, nil); err != nil {
xlog.Error("Failed to start agent", "name", name, "error", err) xlog.Error("Failed to start agent", "name", name, "error", err)
} }
} }
@@ -547,7 +651,7 @@ func (a *AgentPool) Start(name string) error {
return nil return nil
} }
if config, ok := a.pool[name]; ok { if config, ok := a.pool[name]; ok {
return a.startAgentWithConfig(name, &config) return a.startAgentWithConfig(name, &config, nil)
} }
return fmt.Errorf("agent %s not found", name) return fmt.Errorf("agent %s not found", name)

View File

@@ -88,7 +88,7 @@ func (a ActionDefinition) ToFunctionDefinition() *openai.FunctionDefinition {
// Actions is something the agent can do // Actions is something the agent can do
type Action interface { type Action interface {
Run(ctx context.Context, action ActionParams) (ActionResult, error) Run(ctx context.Context, sharedState *AgentSharedState, action ActionParams) (ActionResult, error)
Definition() ActionDefinition Definition() ActionDefinition
Plannable() bool Plannable() bool
} }

15
core/types/filters.go Normal file
View File

@@ -0,0 +1,15 @@
package types
type JobFilter interface {
Name() string
Apply(job *Job) (bool, error)
IsTrigger() bool
}
type JobFilters []JobFilter
type FilterResult struct {
HasTriggers bool `json:"has_triggers"`
TriggeredBy string `json:"triggered_by,omitempty"`
FailedBy string `json:"failed_by,omitempty"`
}

View File

@@ -19,6 +19,7 @@ type Job struct {
ConversationHistory []openai.ChatCompletionMessage ConversationHistory []openai.ChatCompletionMessage
UUID string UUID string
Metadata map[string]interface{} Metadata map[string]interface{}
DoneFilter bool
pastActions []*ActionRequest pastActions []*ActionRequest
nextAction *Action nextAction *Action
@@ -27,6 +28,8 @@ type Job struct {
context context.Context context context.Context
cancel context.CancelFunc cancel context.CancelFunc
Obs *Observable
} }
type ActionRequest struct { type ActionRequest struct {
@@ -159,23 +162,23 @@ func newUUID() string {
// To wait for a Job result, use JobResult.WaitResult() // To wait for a Job result, use JobResult.WaitResult()
func NewJob(opts ...JobOption) *Job { func NewJob(opts ...JobOption) *Job {
j := &Job{ j := &Job{
Result: NewJobResult(), Result: NewJobResult(),
UUID: newUUID(), UUID: uuid.New().String(),
} Metadata: make(map[string]interface{}),
for _, o := range opts { context: context.Background(),
o(j) ConversationHistory: []openai.ChatCompletionMessage{},
} }
var ctx context.Context for _, opt := range opts {
if j.context == nil { opt(j)
ctx = context.Background()
} else {
ctx = j.context
} }
context, cancel := context.WithCancel(ctx) // Store the original request if it exists in the conversation history
j.context = context
ctx, cancel := context.WithCancel(j.context)
j.context = ctx
j.cancel = cancel j.cancel = cancel
return j return j
} }
@@ -198,3 +201,29 @@ func (j *Job) Cancel() {
func (j *Job) GetContext() context.Context { func (j *Job) GetContext() context.Context {
return j.context return j.context
} }
func WithObservable(obs *Observable) JobOption {
return func(j *Job) {
j.Obs = obs
}
}
// GetEvaluationLoop returns the current evaluation loop count
func (j *Job) GetEvaluationLoop() int {
if j.Metadata == nil {
j.Metadata = make(map[string]interface{})
}
if loop, ok := j.Metadata["evaluation_loop"].(int); ok {
return loop
}
return 0
}
// IncrementEvaluationLoop increments the evaluation loop count
func (j *Job) IncrementEvaluationLoop() {
if j.Metadata == nil {
j.Metadata = make(map[string]interface{})
}
currentLoop := j.GetEvaluationLoop()
j.Metadata["evaluation_loop"] = currentLoop + 1
}

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

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

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

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

View File

@@ -12,6 +12,16 @@ services:
- /dev/dri/card1 - /dev/dri/card1
- /dev/dri/renderD129 - /dev/dri/renderD129
mcpbox:
extends:
file: docker-compose.yaml
service: mcpbox
dind:
extends:
file: docker-compose.yaml
service: dind
localrecall: localrecall:
extends: extends:
file: docker-compose.yaml file: docker-compose.yaml

View File

@@ -17,6 +17,16 @@ services:
count: 1 count: 1
capabilities: [gpu] capabilities: [gpu]
mcpbox:
extends:
file: docker-compose.yaml
service: mcpbox
dind:
extends:
file: docker-compose.yaml
service: dind
localrecall: localrecall:
extends: extends:
file: docker-compose.yaml file: docker-compose.yaml
@@ -30,4 +40,4 @@ services:
localagi: localagi:
extends: extends:
file: docker-compose.yaml file: docker-compose.yaml
service: localagi service: localagi

View File

@@ -7,7 +7,7 @@ services:
# Image list (dockerhub): https://hub.docker.com/r/localai/localai # Image list (dockerhub): https://hub.docker.com/r/localai/localai
image: localai/localai:master-ffmpeg-core image: localai/localai:master-ffmpeg-core
command: command:
- ${MODEL_NAME:-arcee-agent} - ${MODEL_NAME:-gemma-3-12b-it-qat}
- ${MULTIMODAL_MODEL:-minicpm-v-2_6} - ${MULTIMODAL_MODEL:-minicpm-v-2_6}
- ${IMAGE_MODEL:-sd-1.5-ggml} - ${IMAGE_MODEL:-sd-1.5-ggml}
- granite-embedding-107m-multilingual - granite-embedding-107m-multilingual
@@ -46,12 +46,44 @@ services:
image: busybox image: busybox
command: ["sh", "-c", "until wget -q -O - http://localrecall:8080 > /dev/null 2>&1; do echo 'Waiting for localrecall...'; sleep 1; done; echo 'localrecall is up!'"] command: ["sh", "-c", "until wget -q -O - http://localrecall:8080 > /dev/null 2>&1; do echo 'Waiting for localrecall...'; sleep 1; done; echo 'localrecall is up!'"]
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
environment:
- DOCKER_TLS_CERTDIR=""
healthcheck:
test: ["CMD", "docker", "info"]
interval: 10s
timeout: 5s
retries: 3
localagi: localagi:
depends_on: depends_on:
localai: localai:
condition: service_healthy condition: service_healthy
localrecall-healthcheck: localrecall-healthcheck:
condition: service_completed_successfully condition: service_completed_successfully
mcpbox:
condition: service_healthy
build: build:
context: . context: .
dockerfile: Dockerfile.webui dockerfile: Dockerfile.webui
@@ -59,7 +91,7 @@ services:
- 8080:3000 - 8080:3000
#image: quay.io/mudler/localagi:master #image: quay.io/mudler/localagi:master
environment: environment:
- LOCALAGI_MODEL=${MODEL_NAME:-arcee-agent} - LOCALAGI_MODEL=${MODEL_NAME:-gemma-3-12b-it-qat}
- LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-minicpm-v-2_6} - LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-minicpm-v-2_6}
- LOCALAGI_IMAGE_MODEL=${IMAGE_MODEL:-sd-1.5-ggml} - LOCALAGI_IMAGE_MODEL=${IMAGE_MODEL:-sd-1.5-ggml}
- LOCALAGI_LLM_API_URL=http://localai:8080 - LOCALAGI_LLM_API_URL=http://localai:8080
@@ -68,6 +100,7 @@ services:
- LOCALAGI_STATE_DIR=/pool - LOCALAGI_STATE_DIR=/pool
- LOCALAGI_TIMEOUT=5m - LOCALAGI_TIMEOUT=5m
- LOCALAGI_ENABLE_CONVERSATIONS_LOGGING=false - LOCALAGI_ENABLE_CONVERSATIONS_LOGGING=false
- LOCALAGI_MCPBOX_URL=http://mcpbox:8080
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
volumes: volumes:

75
go.mod
View File

@@ -10,70 +10,77 @@ require (
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2 github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2
github.com/donseba/go-htmx v1.12.0 github.com/donseba/go-htmx v1.12.0
github.com/eritikass/githubmarkdownconvertergo v0.1.10 github.com/eritikass/githubmarkdownconvertergo v0.1.10
github.com/go-telegram/bot v1.14.2 github.com/go-telegram/bot v1.15.0
github.com/gofiber/fiber/v2 v2.52.6 github.com/gofiber/fiber/v2 v2.52.6
github.com/gofiber/template/html/v2 v2.1.3 github.com/gofiber/template/html/v2 v2.1.3
github.com/google/go-github/v69 v69.2.0 github.com/google/go-github/v69 v69.2.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/metoro-io/mcp-golang v0.9.0 github.com/gorilla/websocket v1.5.3
github.com/metoro-io/mcp-golang v0.11.0
github.com/onsi/ginkgo/v2 v2.23.4 github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0 github.com/onsi/gomega v1.37.0
github.com/philippgille/chromem-go v0.7.0 github.com/philippgille/chromem-go v0.7.0
github.com/sashabaranov/go-openai v1.38.1 github.com/sashabaranov/go-openai v1.39.1
github.com/slack-go/slack v0.16.0 github.com/slack-go/slack v0.16.0
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
github.com/tmc/langchaingo v0.1.13 github.com/tmc/langchaingo v0.1.13
github.com/traefik/yaegi v0.16.1 github.com/traefik/yaegi v0.16.1
github.com/valyala/fasthttp v1.60.0 github.com/valyala/fasthttp v1.61.0
golang.org/x/crypto v0.37.0 golang.org/x/crypto v0.37.0
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
maunium.net/go/mautrix v0.17.0
mvdan.cc/xurls/v2 v2.6.0 mvdan.cc/xurls/v2 v2.6.0
) )
require ( require (
github.com/PuerkitoBio/goquery v1.8.1 // indirect github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/antchfx/htmlquery v1.3.0 // indirect github.com/antchfx/htmlquery v1.3.4 // indirect
github.com/antchfx/xmlquery v1.3.17 // indirect github.com/antchfx/xmlquery v1.4.4 // indirect
github.com/antchfx/xpath v1.2.4 // indirect github.com/antchfx/xpath v1.3.4 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect github.com/bytedance/sonic v1.13.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/gin-gonic/gin v1.8.1 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
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-logr/logr v1.4.2 // indirect
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.9.7 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/gocolly/colly v1.2.0 // indirect github.com/gocolly/colly v1.2.0 // indirect
github.com/gofiber/template v1.8.3 // indirect github.com/gofiber/template v1.8.3 // indirect
github.com/gofiber/utils v1.1.0 // indirect github.com/gofiber/utils v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/invopop/jsonschema v0.13.0 // indirect
github.com/invopop/jsonschema v0.12.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/kennygrant/sanitize v1.2.4 // indirect github.com/kennygrant/sanitize v1.2.4 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pkoukk/tiktoken-go v0.1.6 // indirect github.com/pkoukk/tiktoken-go v0.1.7 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.31.0 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/temoto/robotstxt v1.1.2 // indirect github.com/temoto/robotstxt v1.1.2 // indirect
@@ -81,17 +88,21 @@ require (
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
github.com/ugorji/go/codec v1.2.7 // 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/valyala/bytebufferpool v1.0.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // 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 go.uber.org/automaxprocs v1.6.0 // indirect
golang.org/x/net v0.38.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.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/text v0.24.0 // indirect
golang.org/x/tools v0.31.0 // indirect golang.org/x/tools v0.32.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.36.5 // indirect google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
maunium.net/go/maulogger/v2 v2.4.1 // indirect
) )

284
go.sum
View File

@@ -1,72 +1,73 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 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.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/antchfx/htmlquery v1.3.4 h1:Isd0srPkni2iNTWCwVj/72t7uCphFeor5Q8nCzj1jdQ=
github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E= github.com/antchfx/htmlquery v1.3.4/go.mod h1:K9os0BwIEmLAvTqaNSua8tXLWRWZpocZIH73OzWQbwM=
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8= github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
github.com/antchfx/xmlquery v1.3.17 h1:d0qWjPp/D+vtRw7ivCwT5ApH/3CkQU8JOeo3245PpTk= github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
github.com/antchfx/xmlquery v1.3.17/go.mod h1:Afkq4JIeXut75taLSuI31ISJ/zeq+3jG7TunF7noreA= github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4=
github.com/antchfx/xpath v1.2.4 h1:dW1HB/JxKvGtJ9WyVGJ0sIoEcqftV3SqIstujI+B9XY= github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.2.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 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= 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 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 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/chasefleming/elem-go v0.30.0 h1:BlhV1ekv1RbFiM8XZUQeln1Ikb4D+bu2eDO4agREvok= 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/chasefleming/elem-go v0.30.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2 h1:flLYmnQFZNo04x2NPehMbf30m7Pli57xwZ0NFqR/hb0= 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/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.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 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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 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 h1:7tESER0uxaqsuGMv3yP3pK1drfBUXM6apG4H7/3+IgE=
github.com/donseba/go-htmx v1.12.0/go.mod h1:8PTAYvNKf8+QYis+DpAsggKz+sa2qljtMgvdAeNBh5s= github.com/donseba/go-htmx v1.12.0/go.mod h1:8PTAYvNKf8+QYis+DpAsggKz+sa2qljtMgvdAeNBh5s=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/eritikass/githubmarkdownconvertergo v0.1.10 h1:mL93ADvYMOeT15DcGtK9AaFFc+RcWcy6kQBC6yS/5f4= github.com/eritikass/githubmarkdownconvertergo v0.1.10 h1:mL93ADvYMOeT15DcGtK9AaFFc+RcWcy6kQBC6yS/5f4=
github.com/eritikass/githubmarkdownconvertergo v0.1.10/go.mod h1:BdpHs6imOtzE5KorbUtKa6bZ0ZBh1yFcrTTAL8FwDKY= github.com/eritikass/githubmarkdownconvertergo v0.1.10/go.mod h1:BdpHs6imOtzE5KorbUtKa6bZ0ZBh1yFcrTTAL8FwDKY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= 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 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 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-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-telegram/bot v1.14.2 h1:j9hXerxTuvkw7yFi3sF5jjRVGozNVKkMQSKjMeBJ5FY= github.com/go-telegram/bot v1.15.0 h1:/ba5pp084MUhjR5sQDymQ7JNZ001CQa7QjtxLWcuGpg=
github.com/go-telegram/bot v1.14.2/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= github.com/go-telegram/bot v1.15.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 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/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 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 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA= 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.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc= github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
@@ -75,31 +76,17 @@ github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6
github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE= 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 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE= github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE=
@@ -107,45 +94,45 @@ github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMM
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 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/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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 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/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= 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/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 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/metoro-io/mcp-golang v0.9.0 h1:GpFENjieZ/KosTu7CE7tyGI/a2FhiG0nandR0d8B3rE= github.com/metoro-io/mcp-golang v0.11.0 h1:1k+VSE9QaeMTLn0gJ3FgE/DcjsCBsLFnz5eSFbgXUiI=
github.com/metoro-io/mcp-golang v0.9.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0= github.com/metoro-io/mcp-golang v0.11.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -157,30 +144,30 @@ 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/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 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY= 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/philippgille/chromem-go v0.7.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM= github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY= github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sashabaranov/go-openai v1.38.1 h1:TtZabbFQZa1nEni/IhVtDF/WQjVqDgd+cWR5OeddzF8= github.com/sashabaranov/go-openai v1.39.1 h1:TMD4w77Iy9WTFlgnjNaxbAASdsCJ9R/rMdzL+SN14oU=
github.com/sashabaranov/go-openai v1.38.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/sashabaranov/go-openai v1.39.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/slack-go/slack v0.16.0 h1:khp/WCFv+Hb/B/AJaAwvcxKun0hM6grN0bUZ8xG60P8= github.com/slack-go/slack v0.16.0 h1:khp/WCFv+Hb/B/AJaAwvcxKun0hM6grN0bUZ8xG60P8=
github.com/slack-go/slack v0.16.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/slack-go/slack v0.16.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
@@ -190,11 +177,10 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg= github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
@@ -215,77 +201,87 @@ github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1Ca
github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg= github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E= github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY= github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw= github.com/valyala/fasthttp v1.61.0 h1:VV08V0AfoRaFurP1EWKvQQdPTZHiUzaVoulX1aBDgzU=
github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuHgTO4FXCvc= github.com/valyala/fasthttp v1.61.0/go.mod h1:wRIV/4cMwUPWnRcDno9hGnYZGh78QzODFfo1LTUhBog=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= 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/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg= go.mau.fi/util v0.3.0 h1:Lt3lbRXP6ZBqTINK0EieRWor3zEwwwrDT14Z5N8RUCs=
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.mau.fi/util v0.3.0/go.mod h1:9dGsBCCbZJstx16YgnVMVi3O2bOizELoKpugLD4FoGs=
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 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 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=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.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.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -293,62 +289,46 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 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.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g= jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g=
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:OxvTsCwKosqQ1q7B+8FwXqg4rKZ/UG9dUW+g/VL2xH4= jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:OxvTsCwKosqQ1q7B+8FwXqg4rKZ/UG9dUW+g/VL2xH4=
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
maunium.net/go/mautrix v0.17.0 h1:scc1qlUbzPn+wc+3eAPquyD+3gZwwy/hBANBm+iGKK8=
maunium.net/go/mautrix v0.17.0/go.mod h1:j+puTEQCEydlVxhJ/dQP5chfa26TdvBO7X6F3Ataav8=
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI= mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk= 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 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View File

@@ -22,6 +22,8 @@ var withLogs = os.Getenv("LOCALAGI_ENABLE_CONVERSATIONS_LOGGING") == "true"
var apiKeysEnv = os.Getenv("LOCALAGI_API_KEYS") var apiKeysEnv = os.Getenv("LOCALAGI_API_KEYS")
var imageModel = os.Getenv("LOCALAGI_IMAGE_MODEL") var imageModel = os.Getenv("LOCALAGI_IMAGE_MODEL")
var conversationDuration = os.Getenv("LOCALAGI_CONVERSATION_DURATION") var conversationDuration = os.Getenv("LOCALAGI_CONVERSATION_DURATION")
var localOperatorBaseURL = os.Getenv("LOCALOPERATOR_BASE_URL")
var mcpboxURL = os.Getenv("LOCALAGI_MCPBOX_URL")
func init() { func init() {
if baseModel == "" { if baseModel == "" {
@@ -60,10 +62,15 @@ func main() {
apiURL, apiURL,
apiKey, apiKey,
stateDir, stateDir,
mcpboxURL,
localRAG, localRAG,
services.Actions, services.Actions(map[string]string{
"browser-agent-runner-base-url": localOperatorBaseURL,
"deep-research-runner-base-url": localOperatorBaseURL,
}),
services.Connectors, services.Connectors,
services.DynamicPrompts, services.DynamicPrompts,
services.Filters,
timeout, timeout,
withLogs, withLogs,
) )

View File

@@ -1,13 +1,33 @@
package llm package llm
import ( import (
"context"
"net/http" "net/http"
"time" "time"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai"
) )
func NewClient(APIKey, URL, timeout string) *openai.Client { type LLMClient interface {
CreateChatCompletion(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error)
CreateImage(ctx context.Context, req openai.ImageRequest) (openai.ImageResponse, error)
}
type realClient struct {
*openai.Client
}
func (r *realClient) CreateChatCompletion(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
return r.Client.CreateChatCompletion(ctx, req)
}
func (r *realClient) CreateImage(ctx context.Context, req openai.ImageRequest) (openai.ImageResponse, error) {
return r.Client.CreateImage(ctx, req)
}
// NewClient returns a real OpenAI client as LLMClient
func NewClient(APIKey, URL, timeout string) LLMClient {
// Set up OpenAI client // Set up OpenAI client
if APIKey == "" { if APIKey == "" {
//log.Fatal("OPENAI_API_KEY environment variable not set") //log.Fatal("OPENAI_API_KEY environment variable not set")
@@ -18,11 +38,12 @@ func NewClient(APIKey, URL, timeout string) *openai.Client {
dur, err := time.ParseDuration(timeout) dur, err := time.ParseDuration(timeout)
if err != nil { if err != nil {
xlog.Error("Failed to parse timeout", "error", err)
dur = 150 * time.Second dur = 150 * time.Second
} }
config.HTTPClient = &http.Client{ config.HTTPClient = &http.Client{
Timeout: dur, Timeout: dur,
} }
return openai.NewClientWithConfig(config) return &realClient{openai.NewClientWithConfig(config)}
} }

View File

@@ -10,16 +10,20 @@ import (
"github.com/sashabaranov/go-openai/jsonschema" "github.com/sashabaranov/go-openai/jsonschema"
) )
func GenerateTypedJSON(ctx context.Context, client *openai.Client, guidance, model string, i jsonschema.Definition, dst any) error { func GenerateTypedJSONWithGuidance(ctx context.Context, client LLMClient, guidance, model string, i jsonschema.Definition, dst any) error {
return GenerateTypedJSONWithConversation(ctx, client, []openai.ChatCompletionMessage{
{
Role: "user",
Content: guidance,
},
}, model, i, dst)
}
func GenerateTypedJSONWithConversation(ctx context.Context, client LLMClient, conv []openai.ChatCompletionMessage, model string, i jsonschema.Definition, dst any) error {
toolName := "json" toolName := "json"
decision := openai.ChatCompletionRequest{ decision := openai.ChatCompletionRequest{
Model: model, Model: model,
Messages: []openai.ChatCompletionMessage{ Messages: conv,
{
Role: "user",
Content: guidance,
},
},
Tools: []openai.Tool{ Tools: []openai.Tool{
{ {

25
pkg/llm/mock_client.go Normal file
View File

@@ -0,0 +1,25 @@
package llm
import (
"context"
"github.com/sashabaranov/go-openai"
)
type MockClient struct {
CreateChatCompletionFunc func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error)
CreateImageFunc func(ctx context.Context, req openai.ImageRequest) (openai.ImageResponse, error)
}
func (m *MockClient) CreateChatCompletion(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
if m.CreateChatCompletionFunc != nil {
return m.CreateChatCompletionFunc(ctx, req)
}
return openai.ChatCompletionResponse{}, nil
}
func (m *MockClient) CreateImage(ctx context.Context, req openai.ImageRequest) (openai.ImageResponse, error) {
if m.CreateImageFunc != nil {
return m.CreateImageFunc(ctx, req)
}
return openai.ImageResponse{}, nil
}

149
pkg/localoperator/client.go Normal file
View File

@@ -0,0 +1,149 @@
package localoperator
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type Client struct {
baseURL string
httpClient *http.Client
}
func NewClient(baseURL string, timeout ...time.Duration) *Client {
defaultTimeout := 30 * time.Second
if len(timeout) > 0 {
defaultTimeout = timeout[0]
}
return &Client{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: defaultTimeout,
},
}
}
type AgentRequest struct {
Goal string `json:"goal"`
MaxAttempts int `json:"max_attempts,omitempty"`
MaxNoActionAttempts int `json:"max_no_action_attempts,omitempty"`
}
type DesktopAgentRequest struct {
AgentRequest
DesktopURL string `json:"desktop_url"`
}
type DeepResearchRequest struct {
Topic string `json:"topic"`
MaxCycles int `json:"max_cycles,omitempty"`
MaxNoActionAttempts int `json:"max_no_action_attempts,omitempty"`
MaxResults int `json:"max_results,omitempty"`
}
// Response types
type StateDescription struct {
CurrentURL string `json:"current_url"`
PageTitle string `json:"page_title"`
PageContentDescription string `json:"page_content_description"`
Screenshot string `json:"screenshot"`
ScreenshotMimeType string `json:"screenshot_mime_type"`
}
type StateHistory struct {
States []StateDescription `json:"states"`
}
type DesktopStateDescription struct {
ScreenContent string `json:"screen_content"`
ScreenshotPath string `json:"screenshot_path"`
}
type DesktopStateHistory struct {
States []DesktopStateDescription `json:"states"`
}
type SearchResult struct {
Title string `json:"title"`
URL string `json:"url"`
Description string `json:"description"`
}
type ResearchResult struct {
Topic string `json:"topic"`
Summary string `json:"summary"`
Sources []SearchResult `json:"sources"`
KnowledgeGaps []string `json:"knowledge_gaps"`
SearchQueries []string `json:"search_queries"`
ResearchCycles int `json:"research_cycles"`
CompletionTime time.Duration `json:"completion_time"`
}
func (c *Client) RunBrowserAgent(req AgentRequest) (*StateHistory, error) {
return post[*StateHistory](c.httpClient, c.baseURL+"/api/browser/run", req)
}
func (c *Client) RunDesktopAgent(req DesktopAgentRequest) (*DesktopStateHistory, error) {
return post[*DesktopStateHistory](c.httpClient, c.baseURL+"/api/desktop/run", req)
}
func (c *Client) RunDeepResearch(req DeepResearchRequest) (*ResearchResult, error) {
return post[*ResearchResult](c.httpClient, c.baseURL+"/api/deep-research/run", req)
}
func (c *Client) Readyz() (string, error) {
return c.get("/readyz")
}
func (c *Client) Healthz() (string, error) {
return c.get("/healthz")
}
func (c *Client) get(path string) (string, error) {
resp, err := c.httpClient.Get(c.baseURL + path)
if err != nil {
return "", fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
}
return resp.Status, nil
}
func post[T any](client *http.Client, url string, body interface{}) (T, error) {
var result T
jsonBody, err := json.Marshal(body)
if err != nil {
return result, fmt.Errorf("failed to marshal request body: %w", err)
}
fmt.Println("Sending request", "url", url, "body", string(jsonBody))
resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonBody))
if err != nil {
return result, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
fmt.Println("Response", "status", resp.StatusCode)
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return result, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return result, fmt.Errorf("failed to decode response: %w", err)
}
return result, nil
}

325
pkg/stdio/client.go Normal file
View File

@@ -0,0 +1,325 @@
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
}

View File

@@ -0,0 +1,28 @@
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
pkg/stdio/client_test.go Normal file
View File

@@ -0,0 +1,235 @@
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
pkg/stdio/server.go Normal file
View File

@@ -0,0 +1,473 @@
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,
})
}

View File

@@ -18,8 +18,11 @@ const (
// Actions // Actions
ActionSearch = "search" ActionSearch = "search"
ActionCustom = "custom" ActionCustom = "custom"
ActionBrowserAgentRunner = "browser-agent-runner"
ActionDeepResearchRunner = "deep-research-runner"
ActionGithubIssueLabeler = "github-issue-labeler" ActionGithubIssueLabeler = "github-issue-labeler"
ActionGithubIssueOpener = "github-issue-opener" ActionGithubIssueOpener = "github-issue-opener"
ActionGithubIssueEditor = "github-issue-editor"
ActionGithubIssueCloser = "github-issue-closer" ActionGithubIssueCloser = "github-issue-closer"
ActionGithubIssueSearcher = "github-issue-searcher" ActionGithubIssueSearcher = "github-issue-searcher"
ActionGithubRepositoryGet = "github-repository-get-content" ActionGithubRepositoryGet = "github-repository-get-content"
@@ -32,6 +35,8 @@ const (
ActionGithubPRCreator = "github-pr-creator" ActionGithubPRCreator = "github-pr-creator"
ActionGithubGetAllContent = "github-get-all-repository-content" ActionGithubGetAllContent = "github-get-all-repository-content"
ActionGithubREADME = "github-readme" ActionGithubREADME = "github-readme"
ActionGithubRepositorySearchFiles = "github-repository-search-files"
ActionGithubRepositoryListFiles = "github-repository-list-files"
ActionScraper = "scraper" ActionScraper = "scraper"
ActionWikipedia = "wikipedia" ActionWikipedia = "wikipedia"
ActionBrowse = "browse" ActionBrowse = "browse"
@@ -41,6 +46,7 @@ const (
ActionCounter = "counter" ActionCounter = "counter"
ActionCallAgents = "call_agents" ActionCallAgents = "call_agents"
ActionShellcommand = "shell-command" ActionShellcommand = "shell-command"
ActionSendTelegramMessage = "send-telegram-message"
) )
var AvailableActions = []string{ var AvailableActions = []string{
@@ -48,10 +54,15 @@ var AvailableActions = []string{
ActionCustom, ActionCustom,
ActionGithubIssueLabeler, ActionGithubIssueLabeler,
ActionGithubIssueOpener, ActionGithubIssueOpener,
ActionGithubIssueEditor,
ActionGithubIssueCloser, ActionGithubIssueCloser,
ActionGithubIssueSearcher, ActionGithubIssueSearcher,
ActionGithubRepositoryGet, ActionGithubRepositoryGet,
ActionGithubGetAllContent, ActionGithubGetAllContent,
ActionGithubRepositorySearchFiles,
ActionGithubRepositoryListFiles,
ActionBrowserAgentRunner,
ActionDeepResearchRunner,
ActionGithubRepositoryCreateOrUpdate, ActionGithubRepositoryCreateOrUpdate,
ActionGithubIssueReader, ActionGithubIssueReader,
ActionGithubIssueCommenter, ActionGithubIssueCommenter,
@@ -69,36 +80,44 @@ var AvailableActions = []string{
ActionCounter, ActionCounter,
ActionCallAgents, ActionCallAgents,
ActionShellcommand, ActionShellcommand,
ActionSendTelegramMessage,
} }
func Actions(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action { func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
return func(ctx context.Context, pool *state.AgentPool) []types.Action { return func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
allActions := []types.Action{} return func(ctx context.Context, pool *state.AgentPool) []types.Action {
allActions := []types.Action{}
agentName := a.Name agentName := a.Name
for _, a := range a.Actions { for _, a := range a.Actions {
var config map[string]string var config map[string]string
if err := json.Unmarshal([]byte(a.Config), &config); err != nil { if err := json.Unmarshal([]byte(a.Config), &config); err != nil {
xlog.Error("Error unmarshalling action config", "error", err) xlog.Error("Error unmarshalling action config", "error", err)
continue continue
}
a, err := Action(a.Name, agentName, config, pool, actionsConfigs)
if err != nil {
continue
}
allActions = append(allActions, a)
} }
a, err := Action(a.Name, agentName, config, pool) return allActions
if err != nil {
continue
}
allActions = append(allActions, a)
} }
return allActions
} }
} }
func Action(name, agentName string, config map[string]string, pool *state.AgentPool) (types.Action, error) { func Action(name, agentName string, config map[string]string, pool *state.AgentPool, actionsConfigs map[string]string) (types.Action, error) {
var a types.Action var a types.Action
var err error var err error
if config == nil {
config = map[string]string{}
}
switch name { switch name {
case ActionCustom: case ActionCustom:
a, err = action.NewCustom(config, "") a, err = action.NewCustom(config, "")
@@ -110,10 +129,16 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
a = actions.NewGithubIssueLabeler(config) a = actions.NewGithubIssueLabeler(config)
case ActionGithubIssueOpener: case ActionGithubIssueOpener:
a = actions.NewGithubIssueOpener(config) a = actions.NewGithubIssueOpener(config)
case ActionGithubIssueEditor:
a = actions.NewGithubIssueEditor(config)
case ActionGithubIssueCloser: case ActionGithubIssueCloser:
a = actions.NewGithubIssueCloser(config) a = actions.NewGithubIssueCloser(config)
case ActionGithubIssueSearcher: case ActionGithubIssueSearcher:
a = actions.NewGithubIssueSearch(config) a = actions.NewGithubIssueSearch(config)
case ActionBrowserAgentRunner:
a = actions.NewBrowserAgentRunner(config, actionsConfigs["browser-agent-runner-base-url"])
case ActionDeepResearchRunner:
a = actions.NewDeepResearchRunner(config, actionsConfigs["deep-research-runner-base-url"])
case ActionGithubIssueReader: case ActionGithubIssueReader:
a = actions.NewGithubIssueReader(config) a = actions.NewGithubIssueReader(config)
case ActionGithubPRReader: case ActionGithubPRReader:
@@ -126,6 +151,10 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
a = actions.NewGithubPRCreator(config) a = actions.NewGithubPRCreator(config)
case ActionGithubGetAllContent: case ActionGithubGetAllContent:
a = actions.NewGithubRepositoryGetAllContent(config) a = actions.NewGithubRepositoryGetAllContent(config)
case ActionGithubRepositorySearchFiles:
a = actions.NewGithubRepositorySearchFiles(config)
case ActionGithubRepositoryListFiles:
a = actions.NewGithubRepositoryListFiles(config)
case ActionGithubIssueCommenter: case ActionGithubIssueCommenter:
a = actions.NewGithubIssueCommenter(config) a = actions.NewGithubIssueCommenter(config)
case ActionGithubRepositoryGet: case ActionGithubRepositoryGet:
@@ -150,6 +179,8 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
a = actions.NewCallAgent(config, agentName, pool.InternalAPI()) a = actions.NewCallAgent(config, agentName, pool.InternalAPI())
case ActionShellcommand: case ActionShellcommand:
a = actions.NewShell(config) a = actions.NewShell(config)
case ActionSendTelegramMessage:
a = actions.NewSendTelegramMessageRunner(config)
default: default:
xlog.Error("Action not found", "name", name) xlog.Error("Action not found", "name", name)
return nil, fmt.Errorf("Action not found") return nil, fmt.Errorf("Action not found")
@@ -169,6 +200,16 @@ func ActionsConfigMeta() []config.FieldGroup {
Label: "Search", Label: "Search",
Fields: actions.SearchConfigMeta(), Fields: actions.SearchConfigMeta(),
}, },
{
Name: "browser-agent-runner",
Label: "Browser Agent Runner",
Fields: actions.BrowserAgentRunnerConfigMeta(),
},
{
Name: "deep-research-runner",
Label: "Deep Research Runner",
Fields: actions.DeepResearchRunnerConfigMeta(),
},
{ {
Name: "generate_image", Name: "generate_image",
Label: "Generate Image", Label: "Generate Image",
@@ -184,6 +225,11 @@ func ActionsConfigMeta() []config.FieldGroup {
Label: "GitHub Issue Opener", Label: "GitHub Issue Opener",
Fields: actions.GithubIssueOpenerConfigMeta(), Fields: actions.GithubIssueOpenerConfigMeta(),
}, },
{
Name: "github-issue-editor",
Label: "GitHub Issue Editor",
Fields: actions.GithubIssueEditorConfigMeta(),
},
{ {
Name: "github-issue-closer", Name: "github-issue-closer",
Label: "GitHub Issue Closer", Label: "GitHub Issue Closer",
@@ -214,6 +260,16 @@ func ActionsConfigMeta() []config.FieldGroup {
Label: "GitHub Get All Repository Content", Label: "GitHub Get All Repository Content",
Fields: actions.GithubRepositoryGetAllContentConfigMeta(), 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", Name: "github-repository-create-or-update-content",
Label: "GitHub Repository Create/Update Content", Label: "GitHub Repository Create/Update Content",
@@ -287,7 +343,12 @@ func ActionsConfigMeta() []config.FieldGroup {
{ {
Name: "call_agents", Name: "call_agents",
Label: "Call Agents", Label: "Call Agents",
Fields: []config.Field{}, Fields: actions.CallAgentConfigMeta(),
},
{
Name: "send-telegram-message",
Label: "Send Telegram Message",
Fields: actions.SendTelegramMessageConfigMeta(),
}, },
} }
} }

View File

@@ -18,7 +18,7 @@ func NewBrowse(config map[string]string) *BrowseAction {
type BrowseAction struct{} type BrowseAction struct{}
func (a *BrowseAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (a *BrowseAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
URL string `json:"url"` URL string `json:"url"`
}{} }{}

View File

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

View File

@@ -3,26 +3,56 @@ package actions
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"strings"
"github.com/mudler/LocalAGI/core/state" "github.com/mudler/LocalAGI/core/state"
"github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema" "github.com/sashabaranov/go-openai/jsonschema"
) )
func trimList(list []string) []string {
for i, v := range list {
list[i] = strings.TrimSpace(v)
}
return list
}
func NewCallAgent(config map[string]string, agentName string, pool *state.AgentPoolInternalAPI) *CallAgentAction { func NewCallAgent(config map[string]string, agentName string, pool *state.AgentPoolInternalAPI) *CallAgentAction {
whitelist := []string{}
blacklist := []string{}
if v, ok := config["whitelist"]; ok {
if strings.Contains(v, ",") {
whitelist = trimList(strings.Split(v, ","))
} else {
whitelist = []string{v}
}
}
if v, ok := config["blacklist"]; ok {
if strings.Contains(v, ",") {
blacklist = trimList(strings.Split(v, ","))
} else {
blacklist = []string{v}
}
}
return &CallAgentAction{ return &CallAgentAction{
pool: pool, pool: pool,
myName: agentName, myName: agentName,
whitelist: whitelist,
blacklist: blacklist,
} }
} }
type CallAgentAction struct { type CallAgentAction struct {
pool *state.AgentPoolInternalAPI pool *state.AgentPoolInternalAPI
myName string myName string
whitelist []string
blacklist []string
} }
func (a *CallAgentAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (a *CallAgentAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
AgentName string `json:"agent_name"` AgentName string `json:"agent_name"`
Message string `json:"message"` Message string `json:"message"`
@@ -83,13 +113,32 @@ func (a *CallAgentAction) Run(ctx context.Context, params types.ActionParams) (t
return types.ActionResult{Result: resp.Response, Metadata: metadata}, nil return types.ActionResult{Result: resp.Response, Metadata: metadata}, nil
} }
func (a *CallAgentAction) isAllowedToBeCalled(agentName string) bool {
if agentName == a.myName {
return false
}
if len(a.whitelist) > 0 && len(a.blacklist) > 0 {
return slices.Contains(a.whitelist, agentName) && !slices.Contains(a.blacklist, agentName)
}
if len(a.whitelist) > 0 {
return slices.Contains(a.whitelist, agentName)
}
if len(a.blacklist) > 0 {
return !slices.Contains(a.blacklist, agentName)
}
return true
}
func (a *CallAgentAction) Definition() types.ActionDefinition { func (a *CallAgentAction) Definition() types.ActionDefinition {
allAgents := a.pool.AllAgents() allAgents := a.pool.AllAgents()
agents := []string{} agents := []string{}
for _, ag := range allAgents { for _, ag := range allAgents {
if ag != a.myName { if a.isAllowedToBeCalled(ag) {
agents = append(agents, ag) agents = append(agents, ag)
} }
} }
@@ -125,3 +174,21 @@ func (a *CallAgentAction) Definition() types.ActionDefinition {
func (a *CallAgentAction) Plannable() bool { func (a *CallAgentAction) Plannable() bool {
return true return true
} }
func CallAgentConfigMeta() []config.Field {
return []config.Field{
{
Name: "whitelist",
Label: "Whitelist",
Type: config.FieldTypeText,
Required: false,
HelpText: "Comma-separated list of agent names to call. If not specified, all agents are allowed.",
},
{
Name: "blacklist",
Label: "Blacklist",
Type: config.FieldTypeText,
HelpText: "Comma-separated list of agent names to exclude from the call. If not specified, all agents are allowed.",
},
}
}

View File

@@ -24,7 +24,7 @@ func NewCounter(config map[string]string) *CounterAction {
} }
// Run executes the counter action // Run executes the counter action
func (a *CounterAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (a *CounterAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
// Parse parameters // Parse parameters
request := struct { request := struct {
Name string `json:"name"` Name string `json:"name"`

View File

@@ -0,0 +1,148 @@
package actions
import (
"context"
"fmt"
"time"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
api "github.com/mudler/LocalAGI/pkg/localoperator"
"github.com/sashabaranov/go-openai/jsonschema"
)
const (
MetadataDeepResearchResult = "deep_research_result"
)
type DeepResearchRunner struct {
baseURL, customActionName string
client *api.Client
}
func NewDeepResearchRunner(config map[string]string, defaultURL string) *DeepResearchRunner {
if config["baseURL"] == "" {
config["baseURL"] = defaultURL
}
timeout := "15m"
if config["timeout"] != "" {
timeout = config["timeout"]
}
duration, err := time.ParseDuration(timeout)
if err != nil {
// If parsing fails, use default 15 minutes
duration = 15 * time.Minute
}
client := api.NewClient(config["baseURL"], duration)
return &DeepResearchRunner{
client: client,
baseURL: config["baseURL"],
customActionName: config["customActionName"],
}
}
func (d *DeepResearchRunner) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := api.DeepResearchRequest{}
err := params.Unmarshal(&result)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
}
req := api.DeepResearchRequest{
Topic: result.Topic,
MaxCycles: result.MaxCycles,
MaxNoActionAttempts: result.MaxNoActionAttempts,
MaxResults: result.MaxResults,
}
researchResult, err := d.client.RunDeepResearch(req)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to run deep research: %w", err)
}
// Format the research result into a readable string
var resultStr string
resultStr += "Deep research result\n"
resultStr += fmt.Sprintf("Topic: %s\n", researchResult.Topic)
resultStr += fmt.Sprintf("Summary: %s\n", researchResult.Summary)
resultStr += fmt.Sprintf("Research Cycles: %d\n", researchResult.ResearchCycles)
resultStr += fmt.Sprintf("Completion Time: %s\n\n", researchResult.CompletionTime)
if len(researchResult.Sources) > 0 {
resultStr += "Sources:\n"
for _, source := range researchResult.Sources {
resultStr += fmt.Sprintf("- %s (%s)\n %s\n", source.Title, source.URL, source.Description)
}
}
return types.ActionResult{
Result: fmt.Sprintf("Deep research completed successfully.\n%s", resultStr),
Metadata: map[string]interface{}{MetadataDeepResearchResult: researchResult},
}, nil
}
func (d *DeepResearchRunner) Definition() types.ActionDefinition {
actionName := "run_deep_research"
if d.customActionName != "" {
actionName = d.customActionName
}
description := "Run a deep research on a specific topic, gathering information from multiple sources and providing a comprehensive summary"
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"topic": {
Type: jsonschema.String,
Description: "The topic to research",
},
"max_cycles": {
Type: jsonschema.Number,
Description: "Maximum number of research cycles to perform (optional)",
},
"max_no_action_attempts": {
Type: jsonschema.Number,
Description: "Maximum number of attempts without taking an action (optional)",
},
"max_results": {
Type: jsonschema.Number,
Description: "Maximum number of results to collect (optional)",
},
},
Required: []string{"topic"},
}
}
func (d *DeepResearchRunner) Plannable() bool {
return true
}
// DeepResearchRunnerConfigMeta returns the metadata for Deep Research Runner action configuration fields
func DeepResearchRunnerConfigMeta() []config.Field {
return []config.Field{
{
Name: "baseURL",
Label: "Base URL",
Type: config.FieldTypeText,
Required: false,
HelpText: "Base URL of the LocalOperator API",
},
{
Name: "customActionName",
Label: "Custom Action Name",
Type: config.FieldTypeText,
HelpText: "Custom name for this action",
},
{
Name: "timeout",
Label: "Client Timeout",
Type: config.FieldTypeText,
Required: false,
HelpText: "Client timeout duration (e.g. '15m', '1h'). Defaults to '15m' if not specified.",
},
}
}

View File

@@ -29,7 +29,7 @@ type GenImageAction struct {
imageModel string imageModel string
} }
func (a *GenImageAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (a *GenImageAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
Size string `json:"size"` Size string `json:"size"`

View File

@@ -42,7 +42,7 @@ var _ = Describe("GenImageAction", func() {
"size": "256x256", "size": "256x256",
} }
url, err := action.Run(ctx, params) url, err := action.Run(ctx, nil, params)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(url).ToNot(BeEmpty()) Expect(url).ToNot(BeEmpty())
}) })
@@ -52,7 +52,7 @@ var _ = Describe("GenImageAction", func() {
"size": "256x256", "size": "256x256",
} }
_, err := action.Run(ctx, params) _, err := action.Run(ctx, nil, params)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
}) })

View File

@@ -26,7 +26,7 @@ func NewGithubIssueCloser(config map[string]string) *GithubIssuesCloser {
} }
} }
func (g *GithubIssuesCloser) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (g *GithubIssuesCloser) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Repository string `json:"repository"` Repository string `json:"repository"`
Owner string `json:"owner"` Owner string `json:"owner"`

View File

@@ -27,7 +27,7 @@ func NewGithubIssueCommenter(config map[string]string) *GithubIssuesCommenter {
} }
} }
func (g *GithubIssuesCommenter) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (g *GithubIssuesCommenter) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Repository string `json:"repository"` Repository string `json:"repository"`
Owner string `json:"owner"` Owner string `json:"owner"`

View File

@@ -0,0 +1,151 @@
package actions
import (
"context"
"fmt"
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/sashabaranov/go-openai/jsonschema"
)
type GithubIssueEditor struct {
token, repository, owner, customActionName string
client *github.Client
}
func NewGithubIssueEditor(config map[string]string) *GithubIssueEditor {
client := github.NewClient(nil).WithAuthToken(config["token"])
return &GithubIssueEditor{
client: client,
token: config["token"],
customActionName: config["customActionName"],
repository: config["repository"],
owner: config["owner"],
}
}
func (g *GithubIssueEditor) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`
Description string `json:"description"`
Title string `json:"title"`
IssueNumber int `json:"issue_number"`
}{}
err := params.Unmarshal(&result)
if err != nil {
return types.ActionResult{}, err
}
if g.repository != "" && g.owner != "" {
result.Repository = g.repository
result.Owner = g.owner
}
_, _, err = g.client.Issues.Edit(ctx, result.Owner, result.Repository, result.IssueNumber,
&github.IssueRequest{
Body: &result.Description,
Title: &result.Title,
})
resultString := fmt.Sprintf("Updated issue %d in repository %s/%s", result.IssueNumber, result.Owner, result.Repository)
if err != nil {
resultString = fmt.Sprintf("Error updating issue %d in repository %s/%s: %v", result.IssueNumber, result.Owner, result.Repository, err)
}
return types.ActionResult{Result: resultString}, err
}
func (g *GithubIssueEditor) Definition() types.ActionDefinition {
actionName := "edit_github_issue"
if g.customActionName != "" {
actionName = g.customActionName
}
description := "Edit the title and description of a Github issue in a repository. Use this action after reading the issue"
if g.repository != "" && g.owner != "" {
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"issue_number": {
Type: jsonschema.Number,
Description: "The number of the issue to edit.",
},
"title": {
Type: jsonschema.String,
Description: "The new title for the issue.",
},
"description": {
Type: jsonschema.String,
Description: "The new description for the issue.",
},
},
Required: []string{"issue_number", "title", "description"},
}
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"issue_number": {
Type: jsonschema.Number,
Description: "The number of the issue to edit.",
},
"repository": {
Type: jsonschema.String,
Description: "The repository containing the issue.",
},
"owner": {
Type: jsonschema.String,
Description: "The owner of the repository.",
},
"title": {
Type: jsonschema.String,
Description: "The new title for the issue.",
},
"description": {
Type: jsonschema.String,
Description: "The new description for the issue.",
},
},
Required: []string{"issue_number", "repository", "owner", "title", "description"},
}
}
func (a *GithubIssueEditor) Plannable() bool {
return true
}
// GithubIssueEditorConfigMeta returns the metadata for GitHub Issue Editor action configuration fields
func GithubIssueEditorConfigMeta() []config.Field {
return []config.Field{
{
Name: "token",
Label: "GitHub Token",
Type: config.FieldTypeText,
Required: true,
HelpText: "GitHub API token with repository access",
},
{
Name: "repository",
Label: "Repository",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository name",
},
{
Name: "owner",
Label: "Owner",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository owner",
},
{
Name: "customActionName",
Label: "Custom Action Name",
Type: config.FieldTypeText,
HelpText: "Custom name for this action",
},
}
}

View File

@@ -38,7 +38,7 @@ func NewGithubIssueLabeler(config map[string]string) *GithubIssuesLabeler {
} }
} }
func (g *GithubIssuesLabeler) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (g *GithubIssuesLabeler) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Repository string `json:"repository"` Repository string `json:"repository"`
Owner string `json:"owner"` Owner string `json:"owner"`

View File

@@ -27,7 +27,7 @@ func NewGithubIssueOpener(config map[string]string) *GithubIssuesOpener {
} }
} }
func (g *GithubIssuesOpener) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (g *GithubIssuesOpener) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Title string `json:"title"` Title string `json:"title"`
Body string `json:"text"` Body string `json:"text"`

View File

@@ -27,7 +27,7 @@ func NewGithubIssueReader(config map[string]string) *GithubIssuesReader {
} }
} }
func (g *GithubIssuesReader) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (g *GithubIssuesReader) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Repository string `json:"repository"` Repository string `json:"repository"`
Owner string `json:"owner"` Owner string `json:"owner"`

View File

@@ -28,7 +28,7 @@ func NewGithubIssueSearch(config map[string]string) *GithubIssueSearch {
} }
} }
func (g *GithubIssueSearch) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (g *GithubIssueSearch) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Query string `json:"query"` Query string `json:"query"`
Repository string `json:"repository"` Repository string `json:"repository"`

View File

@@ -3,8 +3,6 @@ package actions
import ( import (
"context" "context"
"fmt" "fmt"
"regexp"
"strconv"
"github.com/google/go-github/v69/github" "github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/core/types"
@@ -17,96 +15,6 @@ type GithubPRCommenter struct {
client *github.Client client *github.Client
} }
var (
patchRegex = regexp.MustCompile(`^@@.*\d [\+\-](\d+),?(\d+)?.+?@@`)
)
type commitFileInfo struct {
FileName string
hunkInfos []*hunkInfo
sha string
}
type hunkInfo struct {
hunkStart int
hunkEnd int
}
func (hi hunkInfo) isLineInHunk(line int) bool {
return line >= hi.hunkStart && line <= hi.hunkEnd
}
func (cfi *commitFileInfo) getHunkInfo(line int) *hunkInfo {
for _, hunkInfo := range cfi.hunkInfos {
if hunkInfo.isLineInHunk(line) {
return hunkInfo
}
}
return nil
}
func (cfi *commitFileInfo) isLineInChange(line int) bool {
return cfi.getHunkInfo(line) != nil
}
func (cfi commitFileInfo) calculatePosition(line int) *int {
hi := cfi.getHunkInfo(line)
if hi == nil {
return nil
}
position := line - hi.hunkStart
return &position
}
func parseHunkPositions(patch, filename string) ([]*hunkInfo, error) {
hunkInfos := make([]*hunkInfo, 0)
if patch != "" {
groups := patchRegex.FindAllStringSubmatch(patch, -1)
if len(groups) < 1 {
return hunkInfos, fmt.Errorf("the patch details for [%s] could not be resolved", filename)
}
for _, patchGroup := range groups {
endPos := 2
if len(patchGroup) > 2 && patchGroup[2] == "" {
endPos = 1
}
hunkStart, err := strconv.Atoi(patchGroup[1])
if err != nil {
hunkStart = -1
}
hunkEnd, err := strconv.Atoi(patchGroup[endPos])
if err != nil {
hunkEnd = -1
}
hunkInfos = append(hunkInfos, &hunkInfo{
hunkStart: hunkStart,
hunkEnd: hunkEnd,
})
}
}
return hunkInfos, nil
}
func getCommitInfo(file *github.CommitFile) (*commitFileInfo, error) {
patch := file.GetPatch()
hunkInfos, err := parseHunkPositions(patch, *file.Filename)
if err != nil {
return nil, err
}
sha := file.GetSHA()
if sha == "" {
return nil, fmt.Errorf("the sha details for [%s] could not be resolved", *file.Filename)
}
return &commitFileInfo{
FileName: *file.Filename,
hunkInfos: hunkInfos,
sha: sha,
}, nil
}
func NewGithubPRCommenter(config map[string]string) *GithubPRCommenter { func NewGithubPRCommenter(config map[string]string) *GithubPRCommenter {
client := github.NewClient(nil).WithAuthToken(config["token"]) client := github.NewClient(nil).WithAuthToken(config["token"])
@@ -119,7 +27,7 @@ func NewGithubPRCommenter(config map[string]string) *GithubPRCommenter {
} }
} }
func (g *GithubPRCommenter) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (g *GithubPRCommenter) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Repository string `json:"repository"` Repository string `json:"repository"`
Owner string `json:"owner"` Owner string `json:"owner"`

View File

@@ -3,6 +3,7 @@ package actions
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"github.com/google/go-github/v69/github" "github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/core/types"
@@ -10,46 +11,103 @@ import (
"github.com/sashabaranov/go-openai/jsonschema" "github.com/sashabaranov/go-openai/jsonschema"
) )
const (
// forkCreationRetries is the number of times to retry checking if a fork is ready
forkCreationRetries = 30
// forkCreationRetryDelay is the duration to wait between fork creation checks
forkCreationRetryDelay = 5 * time.Second
)
type GithubPRCreator struct { type GithubPRCreator struct {
token, repository, owner, customActionName, defaultBranch string token, repository, owner, customActionName, defaultBranch string
useFork bool
client *github.Client client *github.Client
} }
func NewGithubPRCreator(config map[string]string) *GithubPRCreator { func NewGithubPRCreator(config map[string]string) *GithubPRCreator {
client := github.NewClient(nil).WithAuthToken(config["token"]) client := github.NewClient(nil).WithAuthToken(config["token"])
defaultBranch := config["defaultBranch"]
if defaultBranch == "" {
defaultBranch = "main" // Default to "main" if not specified
}
useFork := config["useFork"] == "true"
return &GithubPRCreator{ return &GithubPRCreator{
client: client, client: client,
token: config["token"], token: config["token"],
repository: config["repository"], repository: config["repository"],
owner: config["owner"], owner: config["owner"],
customActionName: config["customActionName"], customActionName: config["customActionName"],
defaultBranch: config["defaultBranch"], defaultBranch: defaultBranch,
useFork: useFork,
} }
} }
func (g *GithubPRCreator) createOrUpdateBranch(ctx context.Context, branchName string) error { // ensureFork ensures that a fork exists for the given repository
// Get the latest commit SHA from the default branch func (g *GithubPRCreator) ensureFork(ctx context.Context, owner, repo string) (string, error) {
ref, _, err := g.client.Git.GetRef(ctx, g.owner, g.repository, "refs/heads/"+g.defaultBranch) // First check if we already have a fork
user, _, err := g.client.Users.Get(ctx, "")
if err != nil { if err != nil {
return fmt.Errorf("failed to get reference: %w", err) return "", fmt.Errorf("failed to get current user: %w", err)
}
forkOwner := user.GetLogin()
// Check if fork already exists
_, _, err = g.client.Repositories.Get(ctx, forkOwner, repo)
if err == nil {
// Fork already exists
return forkOwner, nil
}
// Create fork
_, _, err = g.client.Repositories.CreateFork(ctx, owner, repo, &github.RepositoryCreateForkOptions{})
if err != nil {
return "", fmt.Errorf("failed to create fork: %w", err)
}
// Wait for fork to be ready
for i := 0; i < forkCreationRetries; i++ {
_, _, err = g.client.Repositories.Get(ctx, forkOwner, repo)
if err == nil {
return forkOwner, nil
}
// Sleep for a bit before retrying
time.Sleep(forkCreationRetryDelay)
}
return "", fmt.Errorf("fork creation timed out")
}
func (g *GithubPRCreator) createOrUpdateBranch(ctx context.Context, branchName string, owner string, repository string) error {
// Get the latest commit SHA from the default branch
ref, _, err := g.client.Git.GetRef(ctx, owner, repository, "refs/heads/"+g.defaultBranch)
if err != nil {
return fmt.Errorf("failed to get reference for default branch %s: %w", g.defaultBranch, err)
} }
// Try to get the branch if it exists // Try to get the branch if it exists
_, resp, err := g.client.Git.GetRef(ctx, g.owner, g.repository, "refs/heads/"+branchName) _, resp, err := g.client.Git.GetRef(ctx, owner, repository, "refs/heads/"+branchName)
if err != nil { if err != nil {
// If branch doesn't exist, create it if resp == nil {
if resp != nil && resp.StatusCode == 404 { return fmt.Errorf("failed to check branch existence: %w", err)
}
// If branch doesn't exist (404), create it
if resp.StatusCode == 404 {
newRef := &github.Reference{ newRef := &github.Reference{
Ref: github.String("refs/heads/" + branchName), Ref: github.String("refs/heads/" + branchName),
Object: &github.GitObject{SHA: ref.Object.SHA}, Object: &github.GitObject{SHA: ref.Object.SHA},
} }
_, _, err = g.client.Git.CreateRef(ctx, g.owner, g.repository, newRef) _, _, err = g.client.Git.CreateRef(ctx, owner, repository, newRef)
if err != nil { if err != nil {
return fmt.Errorf("failed to create branch: %w", err) return fmt.Errorf("failed to create branch: %w", err)
} }
return nil return nil
} }
// For other errors, return the error
return fmt.Errorf("failed to check branch existence: %w", err) return fmt.Errorf("failed to check branch existence: %w", err)
} }
@@ -58,7 +116,7 @@ func (g *GithubPRCreator) createOrUpdateBranch(ctx context.Context, branchName s
Ref: github.String("refs/heads/" + branchName), Ref: github.String("refs/heads/" + branchName),
Object: &github.GitObject{SHA: ref.Object.SHA}, Object: &github.GitObject{SHA: ref.Object.SHA},
} }
_, _, err = g.client.Git.UpdateRef(ctx, g.owner, g.repository, updateRef, true) _, _, err = g.client.Git.UpdateRef(ctx, owner, repository, updateRef, true)
if err != nil { if err != nil {
return fmt.Errorf("failed to update branch: %w", err) return fmt.Errorf("failed to update branch: %w", err)
} }
@@ -66,10 +124,10 @@ func (g *GithubPRCreator) createOrUpdateBranch(ctx context.Context, branchName s
return nil return nil
} }
func (g *GithubPRCreator) createOrUpdateFile(ctx context.Context, branch string, filePath string, content string, message string) error { func (g *GithubPRCreator) createOrUpdateFile(ctx context.Context, branch string, filePath string, content string, message string, owner string, repository string) error {
// Get the current file content if it exists // Get the current file content if it exists
var sha *string var sha *string
fileContent, _, _, err := g.client.Repositories.GetContents(ctx, g.owner, g.repository, filePath, &github.RepositoryContentGetOptions{ fileContent, _, _, err := g.client.Repositories.GetContents(ctx, owner, repository, filePath, &github.RepositoryContentGetOptions{
Ref: branch, Ref: branch,
}) })
if err == nil && fileContent != nil { if err == nil && fileContent != nil {
@@ -77,7 +135,7 @@ func (g *GithubPRCreator) createOrUpdateFile(ctx context.Context, branch string,
} }
// Create or update the file // Create or update the file
_, _, err = g.client.Repositories.CreateFile(ctx, g.owner, g.repository, filePath, &github.RepositoryContentFileOptions{ _, _, err = g.client.Repositories.CreateFile(ctx, owner, repository, filePath, &github.RepositoryContentFileOptions{
Message: &message, Message: &message,
Content: []byte(content), Content: []byte(content),
Branch: &branch, Branch: &branch,
@@ -90,7 +148,7 @@ func (g *GithubPRCreator) createOrUpdateFile(ctx context.Context, branch string,
return nil return nil
} }
func (g *GithubPRCreator) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (g *GithubPRCreator) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Repository string `json:"repository"` Repository string `json:"repository"`
Owner string `json:"owner"` Owner string `json:"owner"`
@@ -117,15 +175,29 @@ func (g *GithubPRCreator) Run(ctx context.Context, params types.ActionParams) (t
result.BaseBranch = g.defaultBranch result.BaseBranch = g.defaultBranch
} }
var targetOwner, targetRepo string
if g.useFork {
// Ensure we have a fork and get the fork owner
forkOwner, err := g.ensureFork(ctx, result.Owner, result.Repository)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to ensure fork: %w", err)
}
targetOwner = forkOwner
targetRepo = result.Repository
} else {
targetOwner = result.Owner
targetRepo = result.Repository
}
// Create or update branch // Create or update branch
err = g.createOrUpdateBranch(ctx, result.Branch) err = g.createOrUpdateBranch(ctx, result.Branch, targetOwner, targetRepo)
if err != nil { if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to create/update branch: %w", err) return types.ActionResult{}, fmt.Errorf("failed to create/update branch: %w", err)
} }
// Create or update files // Create or update files
for _, file := range result.Files { for _, file := range result.Files {
err = g.createOrUpdateFile(ctx, result.Branch, file.Path, file.Content, fmt.Sprintf("Update %s", file.Path)) err = g.createOrUpdateFile(ctx, result.Branch, file.Path, file.Content, fmt.Sprintf("Update %s", file.Path), targetOwner, targetRepo)
if err != nil { if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to update file %s: %w", file.Path, err) return types.ActionResult{}, fmt.Errorf("failed to update file %s: %w", file.Path, err)
} }
@@ -134,7 +206,7 @@ func (g *GithubPRCreator) Run(ctx context.Context, params types.ActionParams) (t
// Check if PR already exists for this branch // Check if PR already exists for this branch
prs, _, err := g.client.PullRequests.List(ctx, result.Owner, result.Repository, &github.PullRequestListOptions{ prs, _, err := g.client.PullRequests.List(ctx, result.Owner, result.Repository, &github.PullRequestListOptions{
State: "open", State: "open",
Head: fmt.Sprintf("%s:%s", result.Owner, result.Branch), Head: fmt.Sprintf("%s:%s", targetOwner, result.Branch),
}) })
if err != nil { if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to list pull requests: %w", err) return types.ActionResult{}, fmt.Errorf("failed to list pull requests: %w", err)
@@ -164,6 +236,12 @@ func (g *GithubPRCreator) Run(ctx context.Context, params types.ActionParams) (t
Base: &result.BaseBranch, Base: &result.BaseBranch,
} }
// If using a fork, we need to specify the full head reference
if g.useFork {
head := fmt.Sprintf("%s:%s", targetOwner, result.Branch)
newPR.Head = &head
}
createdPR, _, err := g.client.PullRequests.Create(ctx, result.Owner, result.Repository, newPR) createdPR, _, err := g.client.PullRequests.Create(ctx, result.Owner, result.Repository, newPR)
if err != nil { if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to create pull request: %w", err) return types.ActionResult{}, fmt.Errorf("failed to create pull request: %w", err)
@@ -311,5 +389,12 @@ func GithubPRCreatorConfigMeta() []config.Field {
Required: false, Required: false,
HelpText: "Default branch to create PRs against (defaults to main)", HelpText: "Default branch to create PRs against (defaults to main)",
}, },
{
Name: "useFork",
Label: "Use Fork",
Type: config.FieldTypeCheckbox,
Required: false,
HelpText: "Whether to create PRs from a fork (useful when you don't have write access to the repository). Set to 'true' to enable.",
},
} }
} }

View File

@@ -54,7 +54,7 @@ var _ = Describe("GithubPRCreator", func() {
}, },
} }
result, err := action.Run(ctx, params) result, err := action.Run(ctx, nil, params)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(result.Result).To(ContainSubstring("pull request #")) Expect(result.Result).To(ContainSubstring("pull request #"))
}) })
@@ -65,7 +65,7 @@ var _ = Describe("GithubPRCreator", func() {
"body": "This is a test pull request", "body": "This is a test pull request",
} }
_, err := action.Run(ctx, params) _, err := action.Run(ctx, nil, params)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
}) })

View File

@@ -34,7 +34,7 @@ func NewGithubPRReader(config map[string]string) *GithubPRReader {
} }
} }
func (g *GithubPRReader) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (g *GithubPRReader) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Repository string `json:"repository"` Repository string `json:"repository"`
Owner string `json:"owner"` Owner string `json:"owner"`

View File

@@ -30,7 +30,7 @@ func NewGithubPRReviewer(config map[string]string) *GithubPRReviewer {
} }
} }
func (g *GithubPRReviewer) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (g *GithubPRReviewer) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Repository string `json:"repository"` Repository string `json:"repository"`
Owner string `json:"owner"` Owner string `json:"owner"`

View File

@@ -58,7 +58,7 @@ var _ = Describe("GithubPRReviewer", func() {
}, },
} }
result, err := reviewer.Run(ctx, params) result, err := reviewer.Run(ctx, nil, params)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(result.Result).To(ContainSubstring("reviewed successfully")) Expect(result.Result).To(ContainSubstring("reviewed successfully"))
}) })
@@ -70,7 +70,7 @@ var _ = Describe("GithubPRReviewer", func() {
"review_action": "COMMENT", "review_action": "COMMENT",
} }
result, err := reviewer.Run(ctx, params) result, err := reviewer.Run(ctx, nil, params)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(result.Result).To(ContainSubstring("not found")) Expect(result.Result).To(ContainSubstring("not found"))
}) })
@@ -85,7 +85,7 @@ var _ = Describe("GithubPRReviewer", func() {
"review_action": "INVALID_ACTION", "review_action": "INVALID_ACTION",
} }
_, err := reviewer.Run(ctx, params) _, err := reviewer.Run(ctx, nil, params)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
}) })

View File

@@ -30,7 +30,7 @@ func NewGithubRepositoryCreateOrUpdateContent(config map[string]string) *GithubR
} }
} }
func (g *GithubRepositoryCreateOrUpdateContent) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (g *GithubRepositoryCreateOrUpdateContent) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Path string `json:"path"` Path string `json:"path"`
Repository string `json:"repository"` Repository string `json:"repository"`

View File

@@ -3,11 +3,13 @@ package actions
import ( import (
"context" "context"
"fmt" "fmt"
"path/filepath"
"strings" "strings"
"github.com/google/go-github/v69/github" "github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config" "github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/sashabaranov/go-openai/jsonschema" "github.com/sashabaranov/go-openai/jsonschema"
) )
@@ -28,11 +30,35 @@ func NewGithubRepositoryGetAllContent(config map[string]string) *GithubRepositor
} }
} }
func (g *GithubRepositoryGetAllContent) getContentRecursively(ctx context.Context, path string) (string, error) { // isTextFile checks if a file is likely to be a text file based on its extension
func isTextFile(path string) bool {
// List of common text/code file extensions
textExtensions := map[string]bool{
".txt": true, ".md": true, ".go": true, ".py": true, ".js": true,
".ts": true, ".jsx": true, ".tsx": true, ".html": true, ".css": true,
".json": true, ".yaml": true, ".yml": true, ".xml": true, ".sql": true,
".sh": true, ".bash": true, ".zsh": true, ".rb": true, ".php": true,
".java": true, ".c": true, ".cpp": true, ".h": true, ".hpp": true,
".rs": true, ".swift": true, ".kt": true, ".scala": true, ".lua": true,
".pl": true, ".r": true, ".m": true, ".mm": true, ".f": true,
".f90": true, ".f95": true, ".f03": true, ".f08": true, ".f15": true,
".hs": true, ".lhs": true, ".erl": true, ".hrl": true, ".ex": true,
".exs": true, ".eex": true, ".leex": true, ".heex": true, ".config": true,
".toml": true, ".ini": true, ".conf": true, ".env": true, ".gitignore": true,
".dockerignore": true, ".editorconfig": true, ".prettierrc": true, ".eslintrc": true,
".babelrc": true, ".npmrc": true, ".yarnrc": true, ".lock": true,
}
// Get the file extension
ext := strings.ToLower(filepath.Ext(path))
return textExtensions[ext]
}
func (g *GithubRepositoryGetAllContent) getContentRecursively(ctx context.Context, path string, owner string, repository string) (string, error) {
var result strings.Builder var result strings.Builder
// Get content at the current path // Get content at the current path
_, directoryContent, _, err := g.client.Repositories.GetContents(ctx, g.owner, g.repository, path, nil) _, directoryContent, _, err := g.client.Repositories.GetContents(ctx, owner, repository, path, nil)
if err != nil { if err != nil {
return "", fmt.Errorf("error getting content at path %s: %w", path, err) return "", fmt.Errorf("error getting content at path %s: %w", path, err)
} }
@@ -41,14 +67,21 @@ func (g *GithubRepositoryGetAllContent) getContentRecursively(ctx context.Contex
for _, item := range directoryContent { for _, item := range directoryContent {
if item.GetType() == "dir" { if item.GetType() == "dir" {
// Recursively get content for subdirectories // Recursively get content for subdirectories
subContent, err := g.getContentRecursively(ctx, item.GetPath()) subContent, err := g.getContentRecursively(ctx, item.GetPath(), owner, repository)
if err != nil { if err != nil {
return "", err return "", err
} }
result.WriteString(subContent) result.WriteString(subContent)
} else if item.GetType() == "file" { } else if item.GetType() == "file" {
// Skip binary/image files
if !isTextFile(item.GetPath()) {
xlog.Warn("Skipping non-text file: ", "file", item.GetPath())
result.WriteString(fmt.Sprintf("Skipping non-text file: %s\n", item.GetPath()))
continue
}
// Get file content // Get file content
fileContent, _, _, err := g.client.Repositories.GetContents(ctx, g.owner, g.repository, item.GetPath(), nil) fileContent, _, _, err := g.client.Repositories.GetContents(ctx, owner, repository, item.GetPath(), nil)
if err != nil { if err != nil {
return "", fmt.Errorf("error getting file content for %s: %w", item.GetPath(), err) return "", fmt.Errorf("error getting file content for %s: %w", item.GetPath(), err)
} }
@@ -68,7 +101,7 @@ func (g *GithubRepositoryGetAllContent) getContentRecursively(ctx context.Contex
return result.String(), nil return result.String(), nil
} }
func (g *GithubRepositoryGetAllContent) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (g *GithubRepositoryGetAllContent) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Repository string `json:"repository"` Repository string `json:"repository"`
Owner string `json:"owner"` Owner string `json:"owner"`
@@ -89,7 +122,7 @@ func (g *GithubRepositoryGetAllContent) Run(ctx context.Context, params types.Ac
result.Path = "." result.Path = "."
} }
content, err := g.getContentRecursively(ctx, result.Path) content, err := g.getContentRecursively(ctx, result.Path, result.Owner, result.Repository)
if err != nil { if err != nil {
return types.ActionResult{}, err return types.ActionResult{}, err
} }

View File

@@ -45,7 +45,7 @@ var _ = Describe("GithubRepositoryGetAllContent", func() {
"path": ".", "path": ".",
} }
result, err := action.Run(ctx, params) result, err := action.Run(ctx, nil, params)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(result.Result).NotTo(BeEmpty()) Expect(result.Result).NotTo(BeEmpty())
@@ -64,7 +64,7 @@ var _ = Describe("GithubRepositoryGetAllContent", func() {
"path": "non-existent-path", "path": "non-existent-path",
} }
_, err := action.Run(ctx, params) _, err := action.Run(ctx, nil, params)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
}) })

View File

@@ -27,7 +27,7 @@ func NewGithubRepositoryGetContent(config map[string]string) *GithubRepositoryGe
} }
} }
func (g *GithubRepositoryGetContent) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (g *GithubRepositoryGetContent) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Path string `json:"path"` Path string `json:"path"`
Repository string `json:"repository"` Repository string `json:"repository"`

View File

@@ -0,0 +1,163 @@
package actions
import (
"context"
"fmt"
"strings"
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/sashabaranov/go-openai/jsonschema"
)
type GithubRepositoryListFiles struct {
token, repository, owner, customActionName string
client *github.Client
}
func NewGithubRepositoryListFiles(config map[string]string) *GithubRepositoryListFiles {
client := github.NewClient(nil).WithAuthToken(config["token"])
return &GithubRepositoryListFiles{
client: client,
token: config["token"],
repository: config["repository"],
owner: config["owner"],
customActionName: config["customActionName"],
}
}
func (g *GithubRepositoryListFiles) listFilesRecursively(ctx context.Context, path string, owner string, repository string) ([]string, error) {
var files []string
// Get content at the current path
_, directoryContent, _, err := g.client.Repositories.GetContents(ctx, owner, repository, path, nil)
if err != nil {
return nil, fmt.Errorf("error getting content at path %s: %w", path, err)
}
// Process each item in the directory
for _, item := range directoryContent {
if item.GetType() == "dir" {
// Recursively list files in subdirectories
subFiles, err := g.listFilesRecursively(ctx, item.GetPath(), owner, repository)
if err != nil {
return nil, err
}
files = append(files, subFiles...)
} else if item.GetType() == "file" {
// Add file path to the list
files = append(files, item.GetPath())
}
}
return files, nil
}
func (g *GithubRepositoryListFiles) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`
Path string `json:"path,omitempty"`
}{}
err := params.Unmarshal(&result)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
}
if g.repository != "" && g.owner != "" {
result.Repository = g.repository
result.Owner = g.owner
}
// Start from root if no path specified
if result.Path == "" {
result.Path = "."
}
files, err := g.listFilesRecursively(ctx, result.Path, result.Owner, result.Repository)
if err != nil {
return types.ActionResult{}, err
}
// Join all file paths with newlines for better readability
content := strings.Join(files, "\n")
return types.ActionResult{Result: content}, nil
}
func (g *GithubRepositoryListFiles) Definition() types.ActionDefinition {
actionName := "list_github_repository_files"
if g.customActionName != "" {
actionName = g.customActionName
}
description := "List all files in a GitHub repository"
if g.repository != "" && g.owner != "" {
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"path": {
Type: jsonschema.String,
Description: "Optional path to start listing from (defaults to repository root)",
},
},
}
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"path": {
Type: jsonschema.String,
Description: "Optional path to start listing from (defaults to repository root)",
},
"repository": {
Type: jsonschema.String,
Description: "The repository to list files from",
},
"owner": {
Type: jsonschema.String,
Description: "The owner of the repository",
},
},
Required: []string{"repository", "owner"},
}
}
func (a *GithubRepositoryListFiles) Plannable() bool {
return true
}
// GithubRepositoryListFilesConfigMeta returns the metadata for GitHub Repository List Files action configuration fields
func GithubRepositoryListFilesConfigMeta() []config.Field {
return []config.Field{
{
Name: "token",
Label: "GitHub Token",
Type: config.FieldTypeText,
Required: true,
HelpText: "GitHub API token with repository access",
},
{
Name: "repository",
Label: "Repository",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository name",
},
{
Name: "owner",
Label: "Owner",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository owner",
},
{
Name: "customActionName",
Label: "Custom Action Name",
Type: config.FieldTypeText,
HelpText: "Custom name for this action",
},
}
}

View File

@@ -25,7 +25,7 @@ func NewGithubRepositoryREADME(config map[string]string) *GithubRepositoryREADME
} }
} }
func (g *GithubRepositoryREADME) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (g *GithubRepositoryREADME) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Repository string `json:"repository"` Repository string `json:"repository"`
Owner string `json:"owner"` Owner string `json:"owner"`

View File

@@ -0,0 +1,187 @@
package actions
import (
"context"
"fmt"
"strings"
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/sashabaranov/go-openai/jsonschema"
)
type GithubRepositorySearchFiles struct {
token, repository, owner, customActionName string
client *github.Client
}
func NewGithubRepositorySearchFiles(config map[string]string) *GithubRepositorySearchFiles {
client := github.NewClient(nil).WithAuthToken(config["token"])
return &GithubRepositorySearchFiles{
client: client,
token: config["token"],
repository: config["repository"],
owner: config["owner"],
customActionName: config["customActionName"],
}
}
func (g *GithubRepositorySearchFiles) searchFilesRecursively(ctx context.Context, path string, owner string, repository string, searchPattern string) (string, error) {
var result strings.Builder
// Get content at the current path
_, directoryContent, _, err := g.client.Repositories.GetContents(ctx, owner, repository, path, nil)
if err != nil {
return "", fmt.Errorf("error getting content at path %s: %w", path, err)
}
// Process each item in the directory
for _, item := range directoryContent {
if item.GetType() == "dir" {
// Recursively search in subdirectories
subContent, err := g.searchFilesRecursively(ctx, item.GetPath(), owner, repository, searchPattern)
if err != nil {
return "", err
}
result.WriteString(subContent)
} else if item.GetType() == "file" {
// Check if file name matches the search pattern
if strings.Contains(strings.ToLower(item.GetName()), strings.ToLower(searchPattern)) {
// Get file content
fileContent, _, _, err := g.client.Repositories.GetContents(ctx, owner, repository, item.GetPath(), nil)
if err != nil {
return "", fmt.Errorf("error getting file content for %s: %w", item.GetPath(), err)
}
content, err := fileContent.GetContent()
if err != nil {
return "", fmt.Errorf("error decoding content for %s: %w", item.GetPath(), err)
}
// Add file content to result with clear markers
result.WriteString(fmt.Sprintf("\n--- START FILE: %s ---\n", item.GetPath()))
result.WriteString(content)
result.WriteString(fmt.Sprintf("\n--- END FILE: %s ---\n", item.GetPath()))
}
}
}
return result.String(), nil
}
func (g *GithubRepositorySearchFiles) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`
Path string `json:"path,omitempty"`
SearchPattern string `json:"searchPattern"`
}{}
err := params.Unmarshal(&result)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
}
if g.repository != "" && g.owner != "" {
result.Repository = g.repository
result.Owner = g.owner
}
// Start from root if no path specified
if result.Path == "" {
result.Path = "."
}
content, err := g.searchFilesRecursively(ctx, result.Path, result.Owner, result.Repository, result.SearchPattern)
if err != nil {
return types.ActionResult{}, err
}
return types.ActionResult{Result: content}, nil
}
func (g *GithubRepositorySearchFiles) Definition() types.ActionDefinition {
actionName := "search_github_repository_files"
if g.customActionName != "" {
actionName = g.customActionName
}
description := "Search for files in a GitHub repository and return their content"
if g.repository != "" && g.owner != "" {
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"path": {
Type: jsonschema.String,
Description: "Optional path to start searching from (defaults to repository root)",
},
"searchPattern": {
Type: jsonschema.String,
Description: "Pattern to search for in file names (case-insensitive)",
},
},
Required: []string{"searchPattern"},
}
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"path": {
Type: jsonschema.String,
Description: "Optional path to start searching from (defaults to repository root)",
},
"repository": {
Type: jsonschema.String,
Description: "The repository to search in",
},
"owner": {
Type: jsonschema.String,
Description: "The owner of the repository",
},
"searchPattern": {
Type: jsonschema.String,
Description: "Pattern to search for in file names (case-insensitive)",
},
},
Required: []string{"repository", "owner", "searchPattern"},
}
}
func (a *GithubRepositorySearchFiles) Plannable() bool {
return true
}
// GithubRepositorySearchFilesConfigMeta returns the metadata for GitHub Repository Search Files action configuration fields
func GithubRepositorySearchFilesConfigMeta() []config.Field {
return []config.Field{
{
Name: "token",
Label: "GitHub Token",
Type: config.FieldTypeText,
Required: true,
HelpText: "GitHub API token with repository access",
},
{
Name: "repository",
Label: "Repository",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository name",
},
{
Name: "owner",
Label: "Owner",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository owner",
},
{
Name: "customActionName",
Label: "Custom Action Name",
Type: config.FieldTypeText,
HelpText: "Custom name for this action",
},
}
}

View File

@@ -16,7 +16,7 @@ func NewScraper(config map[string]string) *ScraperAction {
type ScraperAction struct{} type ScraperAction struct{}
func (a *ScraperAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (a *ScraperAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
URL string `json:"url"` URL string `json:"url"`
}{} }{}

View File

@@ -35,7 +35,7 @@ func NewSearch(config map[string]string) *SearchAction {
type SearchAction struct{ results int } type SearchAction struct{ results int }
func (a *SearchAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (a *SearchAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Query string `json:"query"` Query string `json:"query"`
}{} }{}

View File

@@ -28,7 +28,7 @@ type SendMailAction struct {
smtpPort string smtpPort string
} }
func (a *SendMailAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (a *SendMailAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Message string `json:"message"` Message string `json:"message"`
To string `json:"to"` To string `json:"to"`

View File

@@ -0,0 +1,195 @@
package actions
import (
"context"
"fmt"
"strconv"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/xstrings"
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
)
const (
MetadataTelegramMessageSent = "telegram_message_sent"
telegramMaxMessageLength = 3000
)
type SendTelegramMessageRunner struct {
token string
chatID int64
bot *bot.Bot
customName string
customDescription string
}
func NewSendTelegramMessageRunner(config map[string]string) *SendTelegramMessageRunner {
token := config["token"]
if token == "" {
return nil
}
// Parse chat ID from config if present
var chatID int64
if configChatID := config["chat_id"]; configChatID != "" {
var err error
chatID, err = strconv.ParseInt(configChatID, 10, 64)
if err != nil {
return nil
}
}
b, err := bot.New(token)
if err != nil {
return nil
}
return &SendTelegramMessageRunner{
token: token,
chatID: chatID,
bot: b,
customName: config["custom_name"],
customDescription: config["custom_description"],
}
}
type TelegramMessageParams struct {
ChatID int64 `json:"chat_id"`
Message string `json:"message"`
}
func (s *SendTelegramMessageRunner) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
var messageParams TelegramMessageParams
err := params.Unmarshal(&messageParams)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
}
if s.chatID != 0 {
messageParams.ChatID = s.chatID
}
if messageParams.ChatID == 0 {
return types.ActionResult{}, fmt.Errorf("chat_id is required either in config or parameters")
}
if messageParams.Message == "" {
return types.ActionResult{}, fmt.Errorf("message is required")
}
// Split the message if it's too long
messages := xstrings.SplitParagraph(messageParams.Message, telegramMaxMessageLength)
if len(messages) == 0 {
return types.ActionResult{}, fmt.Errorf("empty message after splitting")
}
// Send each message part
for i, msg := range messages {
_, err = s.bot.SendMessage(ctx, &bot.SendMessageParams{
ChatID: messageParams.ChatID,
Text: msg,
ParseMode: models.ParseModeMarkdown,
})
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to send telegram message part %d: %w", i+1, err)
}
}
sharedState.ConversationTracker.AddMessage(fmt.Sprintf("telegram:%d", messageParams.ChatID), openai.ChatCompletionMessage{
Content: messageParams.Message,
Role: "assistant",
})
return types.ActionResult{
Result: fmt.Sprintf("Message sent successfully to chat ID %d in %d parts", messageParams.ChatID, len(messages)),
Metadata: map[string]interface{}{
MetadataTelegramMessageSent: true,
},
}, nil
}
func (s *SendTelegramMessageRunner) Definition() types.ActionDefinition {
customName := "send_telegram_message"
if s.customName != "" {
customName = s.customName
}
customDescription := "Send a message to a Telegram user or group"
if s.customDescription != "" {
customDescription = s.customDescription
}
if s.chatID != 0 {
return types.ActionDefinition{
Name: types.ActionDefinitionName(customName),
Description: customDescription,
Properties: map[string]jsonschema.Definition{
"message": {
Type: jsonschema.String,
Description: "The message to send",
},
},
Required: []string{"message"},
}
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(customName),
Description: customDescription,
Properties: map[string]jsonschema.Definition{
"chat_id": {
Type: jsonschema.Number,
Description: "The Telegram chat ID to send the message to (optional if configured in config)",
},
"message": {
Type: jsonschema.String,
Description: "The message to send",
},
},
Required: []string{"message", "chat_id"},
}
}
func (s *SendTelegramMessageRunner) Plannable() bool {
return true
}
// SendTelegramMessageConfigMeta returns the metadata for Send Telegram Message action configuration fields
func SendTelegramMessageConfigMeta() []config.Field {
return []config.Field{
{
Name: "token",
Label: "Telegram Token",
Type: config.FieldTypeText,
Required: true,
HelpText: "Telegram bot token for sending messages",
},
{
Name: "chat_id",
Label: "Default Chat ID",
Type: config.FieldTypeText,
Required: false,
HelpText: "Default Telegram chat ID to send messages to (can be overridden in parameters)",
},
{
Name: "custom_name",
Label: "Custom Name",
Type: config.FieldTypeText,
Required: false,
HelpText: "Custom name for the action (optional, defaults to 'send_telegram_message')",
},
{
Name: "custom_description",
Label: "Custom Description",
Type: config.FieldTypeText,
Required: false,
HelpText: "Custom description for the action (optional, defaults to 'Send a message to a Telegram user or group')",
},
}
}

View File

@@ -28,7 +28,7 @@ type ShellAction struct {
customDescription string customDescription string
} }
func (a *ShellAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (a *ShellAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Command string `json:"command"` Command string `json:"command"`
Host string `json:"host"` Host string `json:"host"`

View File

@@ -22,7 +22,7 @@ type PostTweetAction struct {
noCharacterLimit bool noCharacterLimit bool
} }
func (a *PostTweetAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (a *PostTweetAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Text string `json:"text"` Text string `json:"text"`
}{} }{}

View File

@@ -15,7 +15,7 @@ func NewWikipedia(config map[string]string) *WikipediaAction {
type WikipediaAction struct{} type WikipediaAction struct{}
func (a *WikipediaAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { func (a *WikipediaAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct { result := struct {
Query string `json:"query"` Query string `json:"query"`
}{} }{}

View File

@@ -19,6 +19,7 @@ const (
ConnectorGithubIssues = "github-issues" ConnectorGithubIssues = "github-issues"
ConnectorGithubPRs = "github-prs" ConnectorGithubPRs = "github-prs"
ConnectorTwitter = "twitter" ConnectorTwitter = "twitter"
ConnectorMatrix = "matrix"
) )
var AvailableConnectors = []string{ var AvailableConnectors = []string{
@@ -29,6 +30,7 @@ var AvailableConnectors = []string{
ConnectorGithubIssues, ConnectorGithubIssues,
ConnectorGithubPRs, ConnectorGithubPRs,
ConnectorTwitter, ConnectorTwitter,
ConnectorMatrix,
} }
func Connectors(a *state.AgentConfig) []state.Connector { func Connectors(a *state.AgentConfig) []state.Connector {
@@ -66,6 +68,8 @@ func Connectors(a *state.AgentConfig) []state.Connector {
continue continue
} }
conns = append(conns, cc) conns = append(conns, cc)
case ConnectorMatrix:
conns = append(conns, connectors.NewMatrix(config))
} }
} }
return conns return conns
@@ -108,5 +112,10 @@ func ConnectorsConfigMeta() []config.FieldGroup {
Label: "Twitter", Label: "Twitter",
Fields: connectors.TwitterConfigMeta(), Fields: connectors.TwitterConfigMeta(),
}, },
{
Name: "matrix",
Label: "Matrix",
Fields: connectors.MatrixConfigMeta(),
},
} }
} }

View File

@@ -2,8 +2,8 @@ package connectors
import ( import (
"encoding/json" "encoding/json"
"fmt"
"strings" "strings"
"time"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/mudler/LocalAGI/core/agent" "github.com/mudler/LocalAGI/core/agent"
@@ -14,9 +14,8 @@ import (
) )
type Discord struct { type Discord struct {
token string token string
defaultChannel string defaultChannel string
conversationTracker *ConversationTracker[string]
} }
// NewDiscord creates a new Discord connector // NewDiscord creates a new Discord connector
@@ -25,15 +24,15 @@ type Discord struct {
// - defaultChannel: Discord channel to always answer even if not mentioned // - defaultChannel: Discord channel to always answer even if not mentioned
func NewDiscord(config map[string]string) *Discord { func NewDiscord(config map[string]string) *Discord {
duration, err := time.ParseDuration(config["lastMessageDuration"]) token := config["token"]
if err != nil {
duration = 5 * time.Minute if !strings.HasPrefix(token, "Bot ") {
token = "Bot " + token
} }
return &Discord{ return &Discord{
conversationTracker: NewConversationTracker[string](duration), token: token,
token: config["token"], defaultChannel: config["defaultChannel"],
defaultChannel: config["defaultChannel"],
} }
} }
@@ -151,12 +150,12 @@ func (d *Discord) handleThreadMessage(a *agent.Agent, s *discordgo.Session, m *d
func (d *Discord) handleChannelMessage(a *agent.Agent, s *discordgo.Session, m *discordgo.MessageCreate) { func (d *Discord) handleChannelMessage(a *agent.Agent, s *discordgo.Session, m *discordgo.MessageCreate) {
d.conversationTracker.AddMessage(m.ChannelID, openai.ChatCompletionMessage{ a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("discord:%s", m.ChannelID), openai.ChatCompletionMessage{
Role: "user", Role: "user",
Content: m.Content, Content: m.Content,
}) })
conv := d.conversationTracker.GetConversation(m.ChannelID) conv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("discord:%s", m.ChannelID))
jobResult := a.Ask( jobResult := a.Ask(
types.WithConversationHistory(conv), types.WithConversationHistory(conv),
@@ -167,7 +166,7 @@ func (d *Discord) handleChannelMessage(a *agent.Agent, s *discordgo.Session, m *
return return
} }
d.conversationTracker.AddMessage(m.ChannelID, openai.ChatCompletionMessage{ a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("discord:%s", m.ChannelID), openai.ChatCompletionMessage{
Role: "assistant", Role: "assistant",
Content: jobResult.Response, Content: jobResult.Response,
}) })

View File

@@ -15,28 +15,22 @@ import (
) )
type IRC struct { type IRC struct {
server string server string
port string port string
nickname string nickname string
channel string channel string
conn *irc.Connection conn *irc.Connection
alwaysReply bool alwaysReply bool
conversationTracker *ConversationTracker[string]
} }
func NewIRC(config map[string]string) *IRC { func NewIRC(config map[string]string) *IRC {
duration, err := time.ParseDuration(config["lastMessageDuration"])
if err != nil {
duration = 5 * time.Minute
}
return &IRC{ return &IRC{
server: config["server"], server: config["server"],
port: config["port"], port: config["port"],
nickname: config["nickname"], nickname: config["nickname"],
channel: config["channel"], channel: config["channel"],
alwaysReply: config["alwaysReply"] == "true", alwaysReply: config["alwaysReply"] == "true",
conversationTracker: NewConversationTracker[string](duration),
} }
} }
@@ -77,8 +71,9 @@ func (i *IRC) Start(a *agent.Agent) {
} }
i.conn.UseTLS = false i.conn.UseTLS = false
i.conn.AddCallback("001", func(e *irc.Event) { i.conn.AddCallback("001", func(e *irc.Event) {
xlog.Info("Connected to IRC server", "server", i.server) xlog.Info("Connected to IRC server", "server", i.server, "arguments", e.Arguments)
i.conn.Join(i.channel) i.conn.Join(i.channel)
i.nickname = e.Arguments[0]
xlog.Info("Joined channel", "channel", i.channel) xlog.Info("Joined channel", "channel", i.channel)
}) })
@@ -114,7 +109,7 @@ func (i *IRC) Start(a *agent.Agent) {
cleanedMessage := cleanUpMessage(message, i.nickname) cleanedMessage := cleanUpMessage(message, i.nickname)
go func() { go func() {
conv := i.conversationTracker.GetConversation(channel) conv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("irc:%s", channel))
conv = append(conv, conv = append(conv,
openai.ChatCompletionMessage{ openai.ChatCompletionMessage{
@@ -124,7 +119,7 @@ func (i *IRC) Start(a *agent.Agent) {
) )
// Update the conversation history // Update the conversation history
i.conversationTracker.AddMessage(channel, openai.ChatCompletionMessage{ a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("irc:%s", channel), openai.ChatCompletionMessage{
Content: cleanedMessage, Content: cleanedMessage,
Role: "user", Role: "user",
}) })
@@ -139,7 +134,7 @@ func (i *IRC) Start(a *agent.Agent) {
} }
// Update the conversation history // Update the conversation history
i.conversationTracker.AddMessage(channel, openai.ChatCompletionMessage{ a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("irc:%s", channel), openai.ChatCompletionMessage{
Content: res.Response, Content: res.Response,
Role: "assistant", Role: "assistant",
}) })
@@ -207,6 +202,13 @@ func (i *IRC) Start(a *agent.Agent) {
// Start the IRC client in a goroutine // Start the IRC client in a goroutine
go i.conn.Loop() go i.conn.Loop()
go func() {
select {
case <-a.Context().Done():
i.conn.Quit()
return
}
}()
} }
// IRCConfigMeta returns the metadata for IRC connector configuration fields // IRCConfigMeta returns the metadata for IRC connector configuration fields
@@ -241,11 +243,5 @@ func IRCConfigMeta() []config.Field {
Label: "Always Reply", Label: "Always Reply",
Type: config.FieldTypeCheckbox, Type: config.FieldTypeCheckbox,
}, },
{
Name: "lastMessageDuration",
Label: "Last Message Duration",
Type: config.FieldTypeText,
DefaultValue: "5m",
},
} }
} }

View File

@@ -0,0 +1,304 @@
package connectors
import (
"context"
"fmt"
"sync"
"time"
"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/sashabaranov/go-openai"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
type Matrix struct {
homeserverURL string
userID string
accessToken string
roomID string
roomMode bool
// To track placeholder messages
placeholders map[string]string // map[jobUUID]messageID
placeholderMutex sync.RWMutex
client *mautrix.Client
// Track active jobs for cancellation
activeJobs map[string][]*types.Job // map[roomID]bool to track if a room has active processing
activeJobsMutex sync.RWMutex
}
const matrixThinkingMessage = "🤔 thinking..."
func NewMatrix(config map[string]string) *Matrix {
return &Matrix{
homeserverURL: config["homeserverURL"],
userID: config["userID"],
accessToken: config["accessToken"],
roomID: config["roomID"],
roomMode: config["roomMode"] == "true",
placeholders: make(map[string]string),
activeJobs: make(map[string][]*types.Job),
}
}
func (m *Matrix) AgentResultCallback() func(state types.ActionState) {
return func(state types.ActionState) {
// Mark the job as completed when we get the final result
if state.ActionCurrentState.Job != nil && state.ActionCurrentState.Job.Metadata != nil {
if room, ok := state.ActionCurrentState.Job.Metadata["room"].(string); ok && room != "" {
m.activeJobsMutex.Lock()
delete(m.activeJobs, room)
m.activeJobsMutex.Unlock()
}
}
}
}
func (m *Matrix) AgentReasoningCallback() func(state types.ActionCurrentState) bool {
return func(state types.ActionCurrentState) bool {
// Check if we have a placeholder message for this job
m.placeholderMutex.RLock()
msgID, exists := m.placeholders[state.Job.UUID]
room := ""
if state.Job.Metadata != nil {
if r, ok := state.Job.Metadata["room"].(string); ok {
room = r
}
}
m.placeholderMutex.RUnlock()
if !exists || msgID == "" || room == "" || m.client == nil {
return true // Skip if we don't have a message to update
}
thought := matrixThinkingMessage + "\n\n"
if state.Reasoning != "" {
thought += "Current thought process:\n" + state.Reasoning
}
// Update the placeholder message with the current reasoning
_, err := m.client.SendText(context.Background(), id.RoomID(room), thought)
if err != nil {
xlog.Error(fmt.Sprintf("Error updating reasoning message: %v", err))
}
return true
}
}
// cancelActiveJobForRoom cancels any active job for the given room
func (m *Matrix) cancelActiveJobForRoom(roomID string) {
m.activeJobsMutex.RLock()
ctxs, exists := m.activeJobs[roomID]
m.activeJobsMutex.RUnlock()
if exists {
xlog.Info(fmt.Sprintf("Cancelling active job for room: %s", roomID))
// Mark the job as inactive
m.activeJobsMutex.Lock()
for _, c := range ctxs {
c.Cancel()
}
delete(m.activeJobs, roomID)
m.activeJobsMutex.Unlock()
}
}
func (m *Matrix) handleRoomMessage(a *agent.Agent, evt *event.Event) {
if m.roomID != evt.RoomID.String() && m.roomMode { // If we have a roomID and it's not the same as the event room
// Skip messages from other rooms
xlog.Info("Skipping reply to room", evt.RoomID, m.roomID)
return
}
if evt.Sender == id.UserID(m.userID) {
// Skip messages from ourselves
return
}
// Skip if message does not mention the bot
mentioned := false
if evt.Content.AsMessage().Mentions != nil {
for _, mention := range evt.Content.AsMessage().Mentions.UserIDs {
if mention == m.client.UserID {
mentioned = true
break
}
}
}
if !mentioned && !m.roomMode {
xlog.Info("Skipping reply because it does not mention the bot", evt.RoomID, m.roomID)
return
}
// Cancel any active job for this room before starting a new one
m.cancelActiveJobForRoom(evt.RoomID.String())
currentConv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("matrix:%s", evt.RoomID.String()))
message := evt.Content.AsMessage().Body
go func() {
agentOptions := []types.JobOption{
types.WithUUID(evt.ID.String()),
}
currentConv = append(currentConv, openai.ChatCompletionMessage{
Role: "user",
Content: message,
})
a.SharedState().ConversationTracker.AddMessage(
fmt.Sprintf("matrix:%s", evt.RoomID.String()), currentConv[len(currentConv)-1],
)
agentOptions = append(agentOptions, types.WithConversationHistory(currentConv))
// Add room to metadata for tracking
metadata := map[string]interface{}{
"room": evt.RoomID.String(),
}
agentOptions = append(agentOptions, types.WithMetadata(metadata))
job := types.NewJob(agentOptions...)
// Mark this room as having an active job
m.activeJobsMutex.Lock()
m.activeJobs[evt.RoomID.String()] = append(m.activeJobs[evt.RoomID.String()], job)
m.activeJobsMutex.Unlock()
defer func() {
// Mark job as complete
m.activeJobsMutex.Lock()
job.Cancel()
for i, j := range m.activeJobs[evt.RoomID.String()] {
if j.UUID == job.UUID {
m.activeJobs[evt.RoomID.String()] = append(m.activeJobs[evt.RoomID.String()][:i], m.activeJobs[evt.RoomID.String()][i+1:]...)
break
}
}
m.activeJobsMutex.Unlock()
}()
res := a.Ask(
agentOptions...,
)
if res.Response == "" {
xlog.Debug(fmt.Sprintf("Empty response from agent"))
return
}
if res.Error != nil {
xlog.Error(fmt.Sprintf("Error from agent: %v", res.Error))
return
}
a.SharedState().ConversationTracker.AddMessage(
fmt.Sprintf("matrix:%s", evt.RoomID.String()), openai.ChatCompletionMessage{
Role: "assistant",
Content: res.Response,
},
)
// Send the response to the room
_, err := m.client.SendText(context.Background(), evt.RoomID, res.Response)
if err != nil {
xlog.Error(fmt.Sprintf("Error sending message: %v", err))
}
}()
}
func (m *Matrix) Start(a *agent.Agent) {
// Create Matrix client
client, err := mautrix.NewClient(m.homeserverURL, id.UserID(m.userID), m.accessToken)
if err != nil {
xlog.Error(fmt.Sprintf("Error creating Matrix client: %v", err))
return
}
xlog.Info("Matrix client created")
m.client = client
// Set up event handler
syncer := client.Syncer.(*mautrix.DefaultSyncer)
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
xlog.Info("Received message", evt.Content.AsMessage().Body)
m.handleRoomMessage(a, evt)
})
syncer.OnEventType(event.StateMember, func(ctx context.Context, evt *event.Event) {
if evt.GetStateKey() == client.UserID.String() && evt.Content.AsMember().Membership == event.MembershipInvite {
_, err := client.JoinRoomByID(ctx, evt.RoomID)
if err != nil {
xlog.Error(fmt.Sprintf("Error joining room: %v", err))
}
xlog.Info(fmt.Sprintf("Joined room: %s (%s)", evt.RoomID.String(), evt.RoomID.URI()))
}
})
syncer.OnEventType(event.EventEncrypted, func(ctx context.Context, evt *event.Event) {
xlog.Info("Received encrypted message, this does not work yet", evt.RoomID.String())
//m.handleRoomMessage(a, evt)
})
// Start syncing
go func() {
for {
err := client.SyncWithContext(a.Context())
xlog.Info("Syncing")
if err != nil {
xlog.Error(fmt.Sprintf("Error syncing: %v", err))
time.Sleep(5 * time.Second)
}
}
}()
// Handle shutdown
go func() {
<-a.Context().Done()
client.StopSync()
}()
}
// MatrixConfigMeta returns the metadata for Matrix connector configuration fields
func MatrixConfigMeta() []config.Field {
return []config.Field{
{
Name: "homeserverURL",
Label: "Homeserver URL",
Type: config.FieldTypeText,
Required: true,
},
{
Name: "userID",
Label: "User ID",
Type: config.FieldTypeText,
Required: true,
},
{
Name: "accessToken",
Label: "Access Token",
Type: config.FieldTypeText,
Required: true,
},
{
Name: "roomID",
Label: "Room ID",
Type: config.FieldTypeText,
},
{
Name: "roomMode",
Label: "Room Mode",
Type: config.FieldTypeCheckbox,
},
}
}

View File

@@ -8,9 +8,9 @@ import (
"os" "os"
"strings" "strings"
"sync" "sync"
"time"
"github.com/mudler/LocalAGI/pkg/config" "github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/localoperator"
"github.com/mudler/LocalAGI/pkg/xlog" "github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/LocalAGI/pkg/xstrings" "github.com/mudler/LocalAGI/pkg/xstrings"
"github.com/mudler/LocalAGI/services/actions" "github.com/mudler/LocalAGI/services/actions"
@@ -41,27 +41,19 @@ type Slack struct {
// Track active jobs for cancellation // Track active jobs for cancellation
activeJobs map[string][]*types.Job // map[channelID]bool to track if a channel has active processing activeJobs map[string][]*types.Job // map[channelID]bool to track if a channel has active processing
activeJobsMutex sync.RWMutex activeJobsMutex sync.RWMutex
conversationTracker *ConversationTracker[string]
} }
const thinkingMessage = ":hourglass: thinking..." const thinkingMessage = ":hourglass: thinking..."
func NewSlack(config map[string]string) *Slack { func NewSlack(config map[string]string) *Slack {
duration, err := time.ParseDuration(config["lastMessageDuration"])
if err != nil {
duration = 5 * time.Minute
}
return &Slack{ return &Slack{
appToken: config["appToken"], appToken: config["appToken"],
botToken: config["botToken"], botToken: config["botToken"],
channelID: config["channelID"], channelID: config["channelID"],
channelMode: config["channelMode"] == "true", channelMode: config["channelMode"] == "true",
conversationTracker: NewConversationTracker[string](duration), placeholders: make(map[string]string),
placeholders: make(map[string]string), activeJobs: make(map[string][]*types.Job),
activeJobs: make(map[string][]*types.Job),
} }
} }
@@ -139,16 +131,6 @@ func cleanUpUsernameFromMessage(message string, b *slack.AuthTestResponse) strin
return cleaned return cleaned
} }
func extractUserIDsFromMessage(message string) []string {
var userIDs []string
for _, part := range strings.Split(message, " ") {
if strings.HasPrefix(part, "<@") && strings.HasSuffix(part, ">") {
userIDs = append(userIDs, strings.TrimPrefix(strings.TrimSuffix(part, ">"), "<@"))
}
}
return userIDs
}
func replaceUserIDsWithNamesInMessage(api *slack.Client, message string) string { func replaceUserIDsWithNamesInMessage(api *slack.Client, message string) string {
for _, part := range strings.Split(message, " ") { for _, part := range strings.Split(message, " ") {
if strings.HasPrefix(part, "<@") && strings.HasSuffix(part, ">") { if strings.HasPrefix(part, "<@") && strings.HasSuffix(part, ">") {
@@ -167,8 +149,38 @@ func replaceUserIDsWithNamesInMessage(api *slack.Client, message string) string
return message return message
} }
func generateAttachmentsFromJobResponse(j *types.JobResult) (attachments []slack.Attachment) { func generateAttachmentsFromJobResponse(j *types.JobResult, api *slack.Client, channelID, ts string) (attachments []slack.Attachment) {
for _, state := range j.State { for _, state := range j.State {
// coming from the browser agent
if history, exists := state.Metadata[actions.MetadataBrowserAgentHistory]; exists {
if historyStruct, ok := history.(*localoperator.StateHistory); ok {
state := historyStruct.States[len(historyStruct.States)-1]
// Decode base64 screenshot and upload to Slack
if state.Screenshot != "" {
screenshotData, err := base64.StdEncoding.DecodeString(state.Screenshot)
if err != nil {
xlog.Error(fmt.Sprintf("Error decoding screenshot: %v", err))
continue
}
data := string(screenshotData)
// Upload the file to Slack
_, err = api.UploadFileV2(slack.UploadFileV2Parameters{
Reader: bytes.NewReader(screenshotData),
FileSize: len(data),
ThreadTimestamp: ts,
Channel: channelID,
Filename: "screenshot.png",
InitialComment: "Browser Agent Screenshot",
})
if err != nil {
xlog.Error(fmt.Sprintf("Error uploading screenshot: %v", err))
continue
}
}
}
}
// coming from the search action // coming from the search action
if urls, exists := state.Metadata[actions.MetadataUrls]; exists { if urls, exists := state.Metadata[actions.MetadataUrls]; exists {
for _, url := range xstrings.UniqueSlice(urls.([]string)) { for _, url := range xstrings.UniqueSlice(urls.([]string)) {
@@ -248,7 +260,7 @@ func (t *Slack) handleChannelMessage(
// Cancel any active job for this channel before starting a new one // Cancel any active job for this channel before starting a new one
t.cancelActiveJobForChannel(ev.Channel) t.cancelActiveJobForChannel(ev.Channel)
currentConv := t.conversationTracker.GetConversation(t.channelID) currentConv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("slack:%s", t.channelID))
message := replaceUserIDsWithNamesInMessage(api, cleanUpUsernameFromMessage(ev.Text, b)) message := replaceUserIDsWithNamesInMessage(api, cleanUpUsernameFromMessage(ev.Text, b))
@@ -292,8 +304,8 @@ func (t *Slack) handleChannelMessage(
}) })
} }
t.conversationTracker.AddMessage( a.SharedState().ConversationTracker.AddMessage(
t.channelID, currentConv[len(currentConv)-1], fmt.Sprintf("slack:%s", t.channelID), currentConv[len(currentConv)-1],
) )
agentOptions = append(agentOptions, types.WithConversationHistory(currentConv)) agentOptions = append(agentOptions, types.WithConversationHistory(currentConv))
@@ -339,14 +351,14 @@ func (t *Slack) handleChannelMessage(
return return
} }
t.conversationTracker.AddMessage( a.SharedState().ConversationTracker.AddMessage(
t.channelID, openai.ChatCompletionMessage{ fmt.Sprintf("slack:%s", t.channelID), openai.ChatCompletionMessage{
Role: "assistant", Role: "assistant",
Content: res.Response, Content: res.Response,
}, },
) )
xlog.Debug("After adding message to conversation tracker", "conversation", t.conversationTracker.GetConversation(t.channelID)) xlog.Debug("After adding message to conversation tracker", "conversation", a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("slack:%s", t.channelID)))
//res.Response = githubmarkdownconvertergo.Slack(res.Response) //res.Response = githubmarkdownconvertergo.Slack(res.Response)
@@ -375,7 +387,7 @@ func replyWithPostMessage(finalResponse string, api *slack.Client, ev *slackeven
slack.MsgOptionEnableLinkUnfurl(), slack.MsgOptionEnableLinkUnfurl(),
slack.MsgOptionText(message, true), slack.MsgOptionText(message, true),
slack.MsgOptionPostMessageParameters(postMessageParams), slack.MsgOptionPostMessageParameters(postMessageParams),
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...), slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res, api, ev.Channel, "")...),
) )
if err != nil { if err != nil {
xlog.Error(fmt.Sprintf("Error posting message: %v", err)) xlog.Error(fmt.Sprintf("Error posting message: %v", err))
@@ -387,7 +399,7 @@ func replyWithPostMessage(finalResponse string, api *slack.Client, ev *slackeven
slack.MsgOptionEnableLinkUnfurl(), slack.MsgOptionEnableLinkUnfurl(),
slack.MsgOptionText(res.Response, true), slack.MsgOptionText(res.Response, true),
slack.MsgOptionPostMessageParameters(postMessageParams), slack.MsgOptionPostMessageParameters(postMessageParams),
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...), slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res, api, ev.Channel, "")...),
// slack.MsgOptionTS(ts), // slack.MsgOptionTS(ts),
) )
if err != nil { if err != nil {
@@ -408,7 +420,7 @@ func replyToUpdateMessage(finalResponse string, api *slack.Client, ev *slackeven
slack.MsgOptionLinkNames(true), slack.MsgOptionLinkNames(true),
slack.MsgOptionEnableLinkUnfurl(), slack.MsgOptionEnableLinkUnfurl(),
slack.MsgOptionText(messages[0], true), slack.MsgOptionText(messages[0], true),
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...), slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res, api, ev.Channel, msgTs)...),
) )
if err != nil { if err != nil {
xlog.Error(fmt.Sprintf("Error updating final message: %v", err)) xlog.Error(fmt.Sprintf("Error updating final message: %v", err))
@@ -435,7 +447,7 @@ func replyToUpdateMessage(finalResponse string, api *slack.Client, ev *slackeven
slack.MsgOptionLinkNames(true), slack.MsgOptionLinkNames(true),
slack.MsgOptionEnableLinkUnfurl(), slack.MsgOptionEnableLinkUnfurl(),
slack.MsgOptionText(finalResponse, true), slack.MsgOptionText(finalResponse, true),
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...), slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res, api, ev.Channel, msgTs)...),
) )
if err != nil { if err != nil {
xlog.Error(fmt.Sprintf("Error updating final message: %v", err)) xlog.Error(fmt.Sprintf("Error updating final message: %v", err))
@@ -721,6 +733,13 @@ func (t *Slack) Start(a *agent.Agent) {
if err != nil { if err != nil {
xlog.Error(fmt.Sprintf("Error posting message: %v", err)) xlog.Error(fmt.Sprintf("Error posting message: %v", err))
} }
a.SharedState().ConversationTracker.AddMessage(
fmt.Sprintf("slack:%s", t.channelID),
openai.ChatCompletionMessage{
Content: ccm.Content,
Role: "assistant",
},
)
}) })
} }
@@ -804,11 +823,5 @@ func SlackConfigMeta() []config.Field {
Label: "Always Reply", Label: "Always Reply",
Type: config.FieldTypeCheckbox, Type: config.FieldTypeCheckbox,
}, },
{
Name: "lastMessageDuration",
Label: "Last Message Duration",
Type: config.FieldTypeText,
DefaultValue: "5m",
},
} }
} }

View File

@@ -1,143 +1,377 @@
package connectors package connectors
import ( import (
"bytes"
"context" "context"
"encoding/base64"
"errors" "errors"
"fmt"
"io"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"slices" "slices"
"strings" "strings"
"time" "sync"
"github.com/go-telegram/bot" "github.com/go-telegram/bot"
"github.com/go-telegram/bot/models" "github.com/go-telegram/bot/models"
"github.com/mudler/LocalAGI/core/agent" "github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config" "github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/localoperator"
"github.com/mudler/LocalAGI/pkg/xlog" "github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/LocalAGI/pkg/xstrings" "github.com/mudler/LocalAGI/pkg/xstrings"
"github.com/mudler/LocalAGI/services/actions" "github.com/mudler/LocalAGI/services/actions"
"github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai"
) )
const telegramThinkingMessage = "🤔 thinking..."
const telegramMaxMessageLength = 3000
type Telegram struct { type Telegram struct {
Token string Token string
bot *bot.Bot bot *bot.Bot
agent *agent.Agent agent *agent.Agent
currentconversation map[int64][]openai.ChatCompletionMessage
lastMessageTime map[int64]time.Time
lastMessageDuration time.Duration
admins []string admins []string
conversationTracker *ConversationTracker[int64] // To track placeholder messages
placeholders map[string]int // map[jobUUID]messageID
placeholderMutex sync.RWMutex
// Track active jobs for cancellation
activeJobs map[int64][]*types.Job // map[chatID]bool to track if a chat has active processing
activeJobsMutex sync.RWMutex
channelID string
} }
// Send any text message to the bot after the bot has been started // Send any text message to the bot after the bot has been started
func (t *Telegram) AgentResultCallback() func(state types.ActionState) { func (t *Telegram) AgentResultCallback() func(state types.ActionState) {
return func(state types.ActionState) { return func(state types.ActionState) {
t.bot.SetMyDescription(t.agent.Context(), &bot.SetMyDescriptionParams{ // Mark the job as completed when we get the final result
Description: state.Reasoning, if state.ActionCurrentState.Job != nil && state.ActionCurrentState.Job.Metadata != nil {
}) if chatID, ok := state.ActionCurrentState.Job.Metadata["chatID"].(int64); ok && chatID != 0 {
t.activeJobsMutex.Lock()
delete(t.activeJobs, chatID)
t.activeJobsMutex.Unlock()
}
}
} }
} }
func (t *Telegram) AgentReasoningCallback() func(state types.ActionCurrentState) bool { func (t *Telegram) AgentReasoningCallback() func(state types.ActionCurrentState) bool {
return func(state types.ActionCurrentState) bool { return func(state types.ActionCurrentState) bool {
t.bot.SetMyDescription(t.agent.Context(), &bot.SetMyDescriptionParams{ // Check if we have a placeholder message for this job
Description: state.Reasoning, t.placeholderMutex.RLock()
msgID, exists := t.placeholders[state.Job.UUID]
chatID := int64(0)
if state.Job.Metadata != nil {
if ch, ok := state.Job.Metadata["chatID"].(int64); ok {
chatID = ch
}
}
t.placeholderMutex.RUnlock()
if !exists || msgID == 0 || chatID == 0 || t.bot == nil {
return true // Skip if we don't have a message to update
}
thought := telegramThinkingMessage + "\n\n"
if state.Reasoning != "" {
thought += "Current thought process:\n" + state.Reasoning
}
// Update the placeholder message with the current reasoning
_, err := t.bot.EditMessageText(t.agent.Context(), &bot.EditMessageTextParams{
ChatID: chatID,
MessageID: msgID,
Text: thought,
}) })
if err != nil {
xlog.Error("Error updating reasoning message", "error", err)
}
return true return true
} }
} }
// cancelActiveJobForChat cancels any active job for the given chat
func (t *Telegram) cancelActiveJobForChat(chatID int64) {
t.activeJobsMutex.RLock()
ctxs, exists := t.activeJobs[chatID]
t.activeJobsMutex.RUnlock()
if exists {
xlog.Info("Cancelling active job for chat", "chatID", chatID)
// Mark the job as inactive
t.activeJobsMutex.Lock()
for _, c := range ctxs {
c.Cancel()
}
delete(t.activeJobs, chatID)
t.activeJobsMutex.Unlock()
}
}
// sendImageToTelegram downloads and sends an image to Telegram
func sendImageToTelegram(ctx context.Context, b *bot.Bot, chatID int64, url string) error {
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("error downloading image: %w", err)
}
defer resp.Body.Close()
// Read the entire body into memory
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("error reading image body: %w", err)
}
// Send image with caption
_, err = b.SendPhoto(ctx, &bot.SendPhotoParams{
ChatID: chatID,
Photo: &models.InputFileUpload{
Filename: "image.jpg",
Data: bytes.NewReader(bodyBytes),
},
Caption: "Generated image",
})
if err != nil {
return fmt.Errorf("error sending photo: %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
for _, state := range res.State {
// Collect URLs from search action
if urlList, exists := state.Metadata[actions.MetadataUrls]; exists {
urls = append(urls, xstrings.UniqueSlice(urlList.([]string))...)
}
// Handle images from gen image actions
if imagesUrls, exists := state.Metadata[actions.MetadataImages]; exists {
for _, url := range xstrings.UniqueSlice(imagesUrls.([]string)) {
xlog.Debug("Sending photo", "url", url)
if err := sendImageToTelegram(ctx, t.bot, chatID, url); err != nil {
xlog.Error("Error handling image", "error", err)
}
}
}
// Handle browser agent screenshots
if history, exists := state.Metadata[actions.MetadataBrowserAgentHistory]; exists {
if historyStruct, ok := history.(*localoperator.StateHistory); ok {
state := historyStruct.States[len(historyStruct.States)-1]
if state.Screenshot != "" {
// Decode base64 screenshot
screenshotData, err := base64.StdEncoding.DecodeString(state.Screenshot)
if err != nil {
xlog.Error("Error decoding screenshot", "error", err)
continue
}
// Send screenshot with caption
_, err = t.bot.SendPhoto(ctx, &bot.SendPhotoParams{
ChatID: chatID,
Photo: &models.InputFileUpload{
Filename: "screenshot.png",
Data: bytes.NewReader(screenshotData),
},
Caption: "Browser Agent Screenshot",
})
if err != nil {
xlog.Error("Error sending screenshot", "error", err)
}
}
}
}
}
return urls, nil
}
// formatResponseWithURLs formats the response text and creates message entities for URLs
func formatResponseWithURLs(response string, urls []string) string {
finalResponse := response
if len(urls) > 0 {
finalResponse += "\n\nReferences:\n"
for i, url := range urls {
finalResponse += fmt.Sprintf("🔗 %d. %s\n", i+1, url)
}
}
return bot.EscapeMarkdown(finalResponse)
}
func (t *Telegram) handleUpdate(ctx context.Context, b *bot.Bot, a *agent.Agent, update *models.Update) { func (t *Telegram) handleUpdate(ctx context.Context, b *bot.Bot, a *agent.Agent, update *models.Update) {
username := update.Message.From.Username username := update.Message.From.Username
xlog.Debug("Received message from user", "username", username, "chatID", update.Message.Chat.ID, "message", update.Message.Text)
internalError := func(err error, msg *models.Message) {
xlog.Error("Error updating final message", "error", err)
b.EditMessageText(ctx, &bot.EditMessageTextParams{
ChatID: update.Message.Chat.ID,
MessageID: msg.ID,
Text: "there was an internal error. try again!",
})
}
if len(t.admins) > 0 && !slices.Contains(t.admins, username) { if len(t.admins) > 0 && !slices.Contains(t.admins, username) {
xlog.Info("Unauthorized user", "username", username) xlog.Info("Unauthorized user", "username", username)
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
ChatID: update.Message.Chat.ID,
Text: "you are not authorized to use this bot!",
})
if err != nil {
xlog.Error("Error sending unauthorized message", "error", err)
}
return return
} }
currentConv := t.conversationTracker.GetConversation(update.Message.From.ID) // Cancel any active job for this chat before starting a new one
t.cancelActiveJobForChat(update.Message.Chat.ID)
currentConv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("telegram:%d", update.Message.From.ID))
currentConv = append(currentConv, openai.ChatCompletionMessage{ currentConv = append(currentConv, openai.ChatCompletionMessage{
Content: update.Message.Text, Content: update.Message.Text,
Role: "user", Role: "user",
}) })
t.conversationTracker.AddMessage( a.SharedState().ConversationTracker.AddMessage(
update.Message.From.ID, fmt.Sprintf("telegram:%d", update.Message.From.ID),
openai.ChatCompletionMessage{ openai.ChatCompletionMessage{
Content: update.Message.Text, Content: update.Message.Text,
Role: "user", Role: "user",
}, },
) )
xlog.Info("New message", "username", username, "conversation", currentConv) // Send initial placeholder message
res := a.Ask( msg, err := b.SendMessage(ctx, &bot.SendMessageParams{
types.WithConversationHistory(currentConv), ChatID: update.Message.Chat.ID,
) Text: bot.EscapeMarkdown(telegramThinkingMessage),
ParseMode: models.ParseModeMarkdown,
xlog.Debug("Response", "response", res.Response) })
if err != nil {
if res.Response == "" { xlog.Error("Error sending initial message", "error", err)
xlog.Error("Empty response from agent")
return return
} }
t.conversationTracker.AddMessage( // Store the UUID->placeholder message mapping
update.Message.From.ID, jobUUID := fmt.Sprintf("%d", msg.ID)
t.placeholderMutex.Lock()
t.placeholders[jobUUID] = msg.ID
t.placeholderMutex.Unlock()
// Add chat ID to metadata for tracking
metadata := map[string]interface{}{
"chatID": update.Message.Chat.ID,
}
// Create a new job with the conversation history and metadata
job := types.NewJob(
types.WithConversationHistory(currentConv),
types.WithUUID(jobUUID),
types.WithMetadata(metadata),
)
// Mark this chat as having an active job
t.activeJobsMutex.Lock()
t.activeJobs[update.Message.Chat.ID] = append(t.activeJobs[update.Message.Chat.ID], job)
t.activeJobsMutex.Unlock()
defer func() {
// Mark job as complete
t.activeJobsMutex.Lock()
job.Cancel()
for i, j := range t.activeJobs[update.Message.Chat.ID] {
if j.UUID == job.UUID {
t.activeJobs[update.Message.Chat.ID] = append(t.activeJobs[update.Message.Chat.ID][:i], t.activeJobs[update.Message.Chat.ID][i+1:]...)
break
}
}
t.activeJobsMutex.Unlock()
// Clean up the placeholder map
t.placeholderMutex.Lock()
delete(t.placeholders, jobUUID)
t.placeholderMutex.Unlock()
}()
res := a.Ask(
types.WithConversationHistory(currentConv),
types.WithUUID(jobUUID),
types.WithMetadata(metadata),
)
if res.Response == "" {
xlog.Error("Empty response from agent")
_, err := b.EditMessageText(ctx, &bot.EditMessageTextParams{
ChatID: update.Message.Chat.ID,
MessageID: msg.ID,
Text: "there was an internal error. try again!",
})
if err != nil {
xlog.Error("Error updating error message", "error", err)
}
return
}
a.SharedState().ConversationTracker.AddMessage(
fmt.Sprintf("telegram:%d", update.Message.From.ID),
openai.ChatCompletionMessage{ openai.ChatCompletionMessage{
Content: res.Response, Content: res.Response,
Role: "assistant", Role: "assistant",
}, },
) )
xlog.Debug("Sending message back to telegram", "response", res.Response) // Handle any multimedia content in the response and collect URLs
urls, err := t.handleMultimediaContent(ctx, update.Message.Chat.ID, res)
for _, res := range res.State { if err != nil {
// coming from the search action xlog.Error("Error handling multimedia content", "error", err)
// if urls, exists := res.Metadata[actions.MetadataUrls]; exists {
// for _, url := range uniqueStringSlice(urls.([]string)) {
// }
// }
// coming from the gen image actions
if imagesUrls, exists := res.Metadata[actions.MetadataImages]; exists {
for _, url := range xstrings.UniqueSlice(imagesUrls.([]string)) {
xlog.Debug("Sending photo", "url", url)
resp, err := http.Get(url)
if err != nil {
xlog.Error("Error downloading image", "error", err.Error())
continue
}
defer resp.Body.Close()
_, err = b.SendPhoto(ctx, &bot.SendPhotoParams{
ChatID: update.Message.Chat.ID,
Photo: &models.InputFileUpload{
Filename: "image.jpg",
Data: resp.Body,
},
})
if err != nil {
xlog.Error("Error sending photo", "error", err.Error())
}
}
}
} }
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
// ParseMode: models.ParseModeMarkdown, // Update the message with the final response
ChatID: update.Message.Chat.ID, formattedResponse := formatResponseWithURLs(res.Response, urls)
Text: res.Response,
// Split the message if it's too long
messages := xstrings.SplitParagraph(formattedResponse, telegramMaxMessageLength)
if len(messages) == 0 {
internalError(errors.New("empty response from agent"), msg)
return
}
// Update the first message
_, err = b.EditMessageText(ctx, &bot.EditMessageTextParams{
ChatID: update.Message.Chat.ID,
MessageID: msg.ID,
Text: messages[0],
ParseMode: models.ParseModeMarkdown,
}) })
if err != nil { if err != nil {
xlog.Error("Error sending message", "error", err) internalError(fmt.Errorf("internal error: %w", err), msg)
return
}
// Send additional chunks as new messages
for i := 1; i < len(messages); i++ {
_, err = b.SendMessage(ctx, &bot.SendMessageParams{
ChatID: update.Message.Chat.ID,
Text: messages[i],
ParseMode: models.ParseModeMarkdown,
})
if err != nil {
internalError(fmt.Errorf("internal error: %w", err), msg)
}
} }
} }
@@ -163,18 +397,42 @@ func (t *Telegram) Start(a *agent.Agent) {
b, err := bot.New(t.Token, opts...) b, err := bot.New(t.Token, opts...)
if err != nil { if err != nil {
panic(err) xlog.Error("Error creating bot", "error", err)
return
} }
t.bot = b t.bot = b
t.agent = a t.agent = a
// go func() { // go func() {
// for m := range a.ConversationChannel() { // forc m := range a.ConversationChannel() {
// t.handleNewMessage(ctx, b, m) // t.handleNewMessage(ctx, b, m)
// } // }
// }() // }()
if t.channelID != "" {
// handle new conversations
a.AddSubscriber(func(ccm openai.ChatCompletionMessage) {
xlog.Debug("Subscriber(telegram)", "message", ccm.Content)
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
ChatID: t.channelID,
Text: ccm.Content,
})
if err != nil {
xlog.Error("Error sending message", "error", err)
return
}
t.agent.SharedState().ConversationTracker.AddMessage(
fmt.Sprintf("telegram:%s", t.channelID),
openai.ChatCompletionMessage{
Content: ccm.Content,
Role: "assistant",
},
)
})
}
b.Start(ctx) b.Start(ctx)
} }
@@ -184,11 +442,6 @@ func NewTelegramConnector(config map[string]string) (*Telegram, error) {
return nil, errors.New("token is required") return nil, errors.New("token is required")
} }
duration, err := time.ParseDuration(config["lastMessageDuration"])
if err != nil {
duration = 5 * time.Minute
}
admins := []string{} admins := []string{}
if _, ok := config["admins"]; ok { if _, ok := config["admins"]; ok {
@@ -196,12 +449,11 @@ func NewTelegramConnector(config map[string]string) (*Telegram, error) {
} }
return &Telegram{ return &Telegram{
Token: token, Token: token,
lastMessageDuration: duration, admins: admins,
admins: admins, placeholders: make(map[string]int),
currentconversation: map[int64][]openai.ChatCompletionMessage{}, activeJobs: make(map[int64][]*types.Job),
lastMessageTime: map[int64]time.Time{}, channelID: config["channel_id"],
conversationTracker: NewConversationTracker[int64](duration),
}, nil }, nil
} }
@@ -221,10 +473,10 @@ func TelegramConfigMeta() []config.Field {
HelpText: "Comma-separated list of Telegram usernames that are allowed to interact with the bot", HelpText: "Comma-separated list of Telegram usernames that are allowed to interact with the bot",
}, },
{ {
Name: "lastMessageDuration", Name: "channel_id",
Label: "Last Message Duration", Label: "Channel ID",
Type: config.FieldTypeText, Type: config.FieldTypeText,
DefaultValue: "5m", HelpText: "Telegram channel ID to send messages to if the agent needs to initiate a conversation",
}, },
} }
} }

44
services/filters.go Normal file
View File

@@ -0,0 +1,44 @@
package services
import (
"github.com/mudler/LocalAGI/core/state"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/LocalAGI/services/filters"
)
func Filters(a *state.AgentConfig) types.JobFilters {
var result []types.JobFilter
for _, f := range a.Filters {
var filter types.JobFilter
var err error
switch f.Type {
case filters.FilterRegex:
filter, err = filters.NewRegexFilter(f.Config)
if err != nil {
xlog.Error("Failed to configure regex", "err", err.Error())
continue
}
case filters.FilterClassifier:
filter, err = filters.NewClassifierFilter(f.Config, a)
if err != nil {
xlog.Error("failed to configure classifier", "err", err.Error())
continue
}
default:
xlog.Error("Unrecognized filter type", "type", f.Type)
continue
}
result = append(result, filter)
}
return result
}
// FiltersConfigMeta returns all filter config metas for UI.
func FiltersConfigMeta() []config.FieldGroup {
return []config.FieldGroup{
filters.RegexFilterConfigMeta(),
filters.ClassifierFilterConfigMeta(),
}
}

View File

@@ -0,0 +1,120 @@
package filters
import (
"encoding/json"
"fmt"
"github.com/mudler/LocalAGI/core/state"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/llm"
"github.com/sashabaranov/go-openai/jsonschema"
)
const FilterClassifier = "classifier"
type ClassifierFilter struct {
name string
client llm.LLMClient
model string
description string
allowOnMatch bool
isTrigger bool
}
type ClassifierFilterConfig struct {
Name string `json:"name"`
Model string `json:"model,omitempty"`
APIURL string `json:"api_url,omitempty"`
Description string `json:"description"`
AllowOnMatch bool `json:"allow_on_match"`
IsTrigger bool `json:"is_trigger"`
}
func NewClassifierFilter(configJSON string, a *state.AgentConfig) (*ClassifierFilter, error) {
var cfg ClassifierFilterConfig
if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
return nil, err
}
var model string
if cfg.Model != "" {
model = cfg.Model
} else {
model = a.Model
}
if cfg.Name == "" {
return nil, fmt.Errorf("Classifier with no name")
}
if cfg.Description == "" {
return nil, fmt.Errorf("%s classifier has no description", cfg.Name)
}
apiUrl := a.APIURL
if cfg.APIURL != "" {
apiUrl = cfg.APIURL
}
client := llm.NewClient(a.APIKey, apiUrl, "1m")
return &ClassifierFilter{
name: cfg.Name,
model: model,
description: cfg.Description,
client: client,
allowOnMatch: cfg.AllowOnMatch,
isTrigger: cfg.IsTrigger,
}, nil
}
const fmtT = `
Does the below message fit the description "%s"
%s
`
func (f *ClassifierFilter) Name() string { return f.name }
func (f *ClassifierFilter) Apply(job *types.Job) (bool, error) {
input := extractInputFromJob(job)
guidance := fmt.Sprintf(fmtT, f.description, input)
var result struct {
Asserted bool `json:"answer"`
}
err := llm.GenerateTypedJSONWithGuidance(job.GetContext(), f.client, guidance, f.model, jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"answer": {
Type: jsonschema.Boolean,
Description: "The answer to the first question",
},
},
Required: []string{"answer"},
}, &result)
if err != nil {
return false, err
}
if result.Asserted {
return f.allowOnMatch, nil
}
return !f.allowOnMatch, nil
}
func (f *ClassifierFilter) IsTrigger() bool {
return f.isTrigger
}
func ClassifierFilterConfigMeta() config.FieldGroup {
return config.FieldGroup{
Name: FilterClassifier,
Label: "Classifier Filter/Trigger",
Fields: []config.Field{
{Name: "name", Label: "Name", Type: "text", Required: true},
{Name: "model", Label: "Model", Type: "text", Required: false,
HelpText: "The LLM to use, usually a smaller one. Leave blank to use the same as the agent's"},
{Name: "api_url", Label: "API URL", Type: "url", Required: false,
HelpText: "The URL of the LLM service if different from the agent's"},
{Name: "description", Label: "Description", Type: "text", Required: true,
HelpText: "Describe the type of content to match against e.g. 'technical support request'"},
{Name: "allow_on_match", Label: "Allow on Match", Type: "checkbox", Required: true},
{Name: "is_trigger", Label: "Is Trigger", Type: "checkbox", Required: true},
},
}
}

86
services/filters/regex.go Normal file
View File

@@ -0,0 +1,86 @@
package filters
import (
"encoding/json"
"regexp"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
)
const FilterRegex = "regex"
type RegexFilter struct {
name string
pattern *regexp.Regexp
allowOnMatch bool
isTrigger bool
}
type RegexFilterConfig struct {
Name string `json:"name"`
Pattern string `json:"pattern"`
AllowOnMatch bool `json:"allow_on_match"`
IsTrigger bool `json:"is_trigger"`
}
func NewRegexFilter(configJSON string) (*RegexFilter, error) {
var cfg RegexFilterConfig
if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
return nil, err
}
re, err := regexp.Compile(cfg.Pattern)
if err != nil {
return nil, err
}
return &RegexFilter{
name: cfg.Name,
pattern: re,
allowOnMatch: cfg.AllowOnMatch,
isTrigger: cfg.IsTrigger,
}, nil
}
func (f *RegexFilter) Name() string { return f.name }
func (f *RegexFilter) Apply(job *types.Job) (bool, error) {
input := extractInputFromJob(job)
if f.pattern.MatchString(input) {
return f.allowOnMatch, nil
}
return !f.allowOnMatch, nil
}
func (f *RegexFilter) IsTrigger() bool {
return f.isTrigger
}
func RegexFilterConfigMeta() config.FieldGroup {
return config.FieldGroup{
Name: FilterRegex,
Label: "Regex Filter/Trigger",
Fields: []config.Field{
{Name: "name", Label: "Name", Type: "text", Required: true},
{Name: "pattern", Label: "Pattern", Type: "text", Required: true},
{Name: "allow_on_match", Label: "Allow on Match", Type: "checkbox", Required: true},
{Name: "is_trigger", Label: "Is Trigger", Type: "checkbox", Required: true},
},
}
}
// extractInputFromJob attempts to extract a string input for filtering.
func extractInputFromJob(job *types.Job) string {
if job.Metadata != nil {
if v, ok := job.Metadata["input"]; ok {
if s, ok := v.(string); ok {
return s
}
}
}
// fallback: try to use conversation history if available
if len(job.ConversationHistory) > 0 {
// Use the last message content
last := job.ConversationHistory[len(job.ConversationHistory)-1]
return last.Content
}
return ""
}

View File

@@ -23,6 +23,7 @@ oauth_config:
- commands - commands
- groups:history - groups:history
- files:read - files:read
- files:write
- im:history - im:history
- im:read - im:read
- im:write - im:write

View File

@@ -11,12 +11,14 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/mudler/LocalAGI/core/conversations"
coreTypes "github.com/mudler/LocalAGI/core/types" coreTypes "github.com/mudler/LocalAGI/core/types"
internalTypes "github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/llm" "github.com/mudler/LocalAGI/pkg/llm"
"github.com/mudler/LocalAGI/pkg/xlog" "github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/LocalAGI/services" "github.com/mudler/LocalAGI/services"
"github.com/mudler/LocalAGI/services/connectors"
"github.com/mudler/LocalAGI/webui/types" "github.com/mudler/LocalAGI/webui/types"
"github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema" "github.com/sashabaranov/go-openai/jsonschema"
@@ -33,6 +35,7 @@ type (
htmx *htmx.HTMX htmx *htmx.HTMX
config *Config config *Config
*fiber.App *fiber.App
sharedState *internalTypes.AgentSharedState
} }
) )
@@ -47,9 +50,10 @@ func NewApp(opts ...Option) *App {
}) })
a := &App{ a := &App{
htmx: htmx.New(), htmx: htmx.New(),
config: config, config: config,
App: webapp, App: webapp,
sharedState: internalTypes.NewAgentSharedState(5 * time.Minute),
} }
a.registerRoutes(config.Pool, webapp) a.registerRoutes(config.Pool, webapp)
@@ -176,17 +180,7 @@ func (a *App) UpdateAgentConfig(pool *state.AgentPool) func(c *fiber.Ctx) error
return errorJSONMessage(c, err.Error()) return errorJSONMessage(c, err.Error())
} }
// Remove the agent first if err := pool.RecreateAgent(agentName, &newConfig); err != nil {
if err := pool.Remove(agentName); err != nil {
return errorJSONMessage(c, "Error removing agent: "+err.Error())
}
// Create agent with new config
if err := pool.CreateAgent(agentName, &newConfig); err != nil {
// Try to restore the old configuration if update fails
if restoreErr := pool.CreateAgent(agentName, oldConfig); restoreErr != nil {
return errorJSONMessage(c, fmt.Sprintf("Failed to update agent and restore failed: %v, %v", err, restoreErr))
}
return errorJSONMessage(c, "Error updating agent: "+err.Error()) return errorJSONMessage(c, "Error updating agent: "+err.Error())
} }
@@ -370,7 +364,7 @@ func (a *App) Chat(pool *state.AgentPool) func(c *fiber.Ctx) error {
xlog.Error("Error marshaling status message", "error", err) xlog.Error("Error marshaling status message", "error", err)
} else { } else {
manager.Send( manager.Send(
sse.NewMessage(string(statusData)).WithEvent("json_status")) sse.NewMessage(string(statusData)).WithEvent("json_message_status"))
} }
// Process the message asynchronously // Process the message asynchronously
@@ -417,7 +411,7 @@ func (a *App) Chat(pool *state.AgentPool) func(c *fiber.Ctx) error {
xlog.Error("Error marshaling completed status", "error", err) xlog.Error("Error marshaling completed status", "error", err)
} else { } else {
manager.Send( manager.Send(
sse.NewMessage(string(completedData)).WithEvent("json_status")) sse.NewMessage(string(completedData)).WithEvent("json_message_status"))
} }
}() }()
@@ -429,7 +423,31 @@ func (a *App) Chat(pool *state.AgentPool) func(c *fiber.Ctx) error {
} }
} }
func (a *App) ExecuteAction(pool *state.AgentPool) func(c *fiber.Ctx) error { func (a *App) GetActionDefinition(pool *state.AgentPool) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
payload := struct {
Config map[string]string `json:"config"`
}{}
if err := c.BodyParser(&payload); err != nil {
xlog.Error("Error parsing action payload", "error", err)
return errorJSONMessage(c, err.Error())
}
actionName := c.Params("name")
xlog.Debug("Executing action", "action", actionName, "config", payload.Config)
a, err := services.Action(actionName, "", payload.Config, pool, map[string]string{})
if err != nil {
xlog.Error("Error creating action", "error", err)
return errorJSONMessage(c, err.Error())
}
return c.JSON(a.Definition())
}
}
func (app *App) ExecuteAction(pool *state.AgentPool) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error {
payload := struct { payload := struct {
Config map[string]string `json:"config"` Config map[string]string `json:"config"`
@@ -444,7 +462,7 @@ func (a *App) ExecuteAction(pool *state.AgentPool) func(c *fiber.Ctx) error {
actionName := c.Params("name") actionName := c.Params("name")
xlog.Debug("Executing action", "action", actionName, "config", payload.Config, "params", payload.Params) xlog.Debug("Executing action", "action", actionName, "config", payload.Config, "params", payload.Params)
a, err := services.Action(actionName, "", payload.Config, pool) a, err := services.Action(actionName, "", payload.Config, pool, map[string]string{})
if err != nil { if err != nil {
xlog.Error("Error creating action", "error", err) xlog.Error("Error creating action", "error", err)
return errorJSONMessage(c, err.Error()) return errorJSONMessage(c, err.Error())
@@ -453,7 +471,7 @@ func (a *App) ExecuteAction(pool *state.AgentPool) func(c *fiber.Ctx) error {
ctx, cancel := context.WithTimeout(c.Context(), 200*time.Second) ctx, cancel := context.WithTimeout(c.Context(), 200*time.Second)
defer cancel() defer cancel()
res, err := a.Run(ctx, payload.Params) res, err := a.Run(ctx, app.sharedState, payload.Params)
if err != nil { if err != nil {
xlog.Error("Error running action", "error", err) xlog.Error("Error running action", "error", err)
return errorJSONMessage(c, err.Error()) return errorJSONMessage(c, err.Error())
@@ -470,7 +488,7 @@ func (a *App) ListActions() func(c *fiber.Ctx) error {
} }
} }
func (a *App) Responses(pool *state.AgentPool, tracker *connectors.ConversationTracker[string]) func(c *fiber.Ctx) error { func (a *App) Responses(pool *state.AgentPool, tracker *conversations.ConversationTracker[string]) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error {
var request types.RequestBody var request types.RequestBody
if err := c.BodyParser(&request); err != nil { if err := c.BodyParser(&request); err != nil {
@@ -562,7 +580,7 @@ func (a *App) GenerateGroupProfiles(pool *state.AgentPool) func(c *fiber.Ctx) er
xlog.Debug("Generating group", "description", request.Descript) xlog.Debug("Generating group", "description", request.Descript)
client := llm.NewClient(a.config.LLMAPIKey, a.config.LLMAPIURL, "10m") client := llm.NewClient(a.config.LLMAPIKey, a.config.LLMAPIURL, "10m")
err := llm.GenerateTypedJSON(c.Context(), client, request.Descript, a.config.LLMModel, jsonschema.Definition{ err := llm.GenerateTypedJSONWithGuidance(c.Context(), client, request.Descript, a.config.LLMModel, jsonschema.Definition{
Type: jsonschema.Object, Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{ Properties: map[string]jsonschema.Definition{
"agents": { "agents": {
@@ -630,6 +648,7 @@ func (a *App) GetAgentConfigMeta() func(c *fiber.Ctx) error {
services.ActionsConfigMeta(), services.ActionsConfigMeta(),
services.ConnectorsConfigMeta(), services.ConnectorsConfigMeta(),
services.DynamicPromptsConfigMeta(), services.DynamicPromptsConfigMeta(),
services.FiltersConfigMeta(),
) )
return c.JSON(configMeta) return c.JSON(configMeta)
} }

View File

@@ -4,20 +4,21 @@
"": { "": {
"name": "react-ui", "name": "react-ui",
"dependencies": { "dependencies": {
"highlight.js": "^11.11.1",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.24.0", "@eslint/js": "^9.25.1",
"@types/react": "^19.1.2", "@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2", "@types/react-dom": "^19.1.3",
"@vitejs/plugin-react": "^4.4.0", "@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.24.0", "eslint": "^9.25.1",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^6.0.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.0.0", "globals": "^16.0.0",
"react-router-dom": "^7.5.0", "react-router-dom": "^7.5.3",
"vite": "^6.3.1", "vite": "^6.3.3",
}, },
}, },
}, },
@@ -32,14 +33,26 @@
"@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="], "@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="],
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.0", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.0", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA=="],
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.27.0", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/helper-replace-supers": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/traverse": "^7.27.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg=="],
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="], "@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="], "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="],
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="], "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="],
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.26.5", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/traverse": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg=="],
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
@@ -50,6 +63,8 @@
"@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], "@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="],
"@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.25.9", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="],
@@ -118,11 +133,11 @@
"@eslint/config-helpers": ["@eslint/config-helpers@0.2.1", "", {}, "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw=="], "@eslint/config-helpers": ["@eslint/config-helpers@0.2.1", "", {}, "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw=="],
"@eslint/core": ["@eslint/core@0.12.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg=="], "@eslint/core": ["@eslint/core@0.13.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw=="],
"@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/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.24.0", "", {}, "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA=="], "@eslint/js": ["@eslint/js@9.25.1", "", {}, "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
@@ -194,17 +209,15 @@
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="], "@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/react": ["@types/react@19.1.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw=="], "@types/react": ["@types/react@19.1.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw=="],
"@types/react-dom": ["@types/react-dom@19.1.2", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw=="], "@types/react-dom": ["@types/react-dom@19.1.3", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.4.0", "", { "dependencies": { "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-x/EztcTKVj+TDeANY1WjNeYsvZjZdfWRMP/KXi5Yn8BoTzpa13ZltaQqKfvWYbX8CE10GOHHdC5v86jY9x8i/g=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.4.1", "", { "dependencies": { "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w=="],
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
@@ -254,11 +267,11 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.24.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.0", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.24.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ=="], "eslint": ["eslint@9.25.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.13.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.25.1", "@eslint/plugin-kit": "^0.2.8", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@6.0.0", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "@babel/plugin-transform-private-methods": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-NyC3yIC9fazLitYiN8eHykV5wLp/SMuUZMh+sdPSHIeN4ReXIc7if40jtGjDplAgVL/4OkN1d9gneWe9lFZgag=="],
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.19", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ=="], "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.20", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA=="],
"eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="], "eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="],
@@ -280,7 +293,7 @@
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="], "fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
@@ -300,6 +313,12 @@
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@@ -374,9 +393,9 @@
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
"react-router": ["react-router@7.5.0", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g=="], "react-router": ["react-router@7.5.3", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw=="],
"react-router-dom": ["react-router-dom@7.5.0", "", { "dependencies": { "react-router": "7.5.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA=="], "react-router-dom": ["react-router-dom@7.5.3", "", { "dependencies": { "react-router": "7.5.3" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-cK0jSaTyW4jV9SRKAItMIQfWZ/D6WEZafgHuuCb9g+SjhLolY78qc+De4w/Cz9ybjvLzShAmaIMEXt8iF1Cm+A=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
@@ -398,7 +417,7 @@
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="], "tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
"turbo-stream": ["turbo-stream@2.4.0", "", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="], "turbo-stream": ["turbo-stream@2.4.0", "", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="],
@@ -408,7 +427,7 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"vite": ["vite@6.3.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.3", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.12" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-kkzzkqtMESYklo96HKKPE5KKLkC1amlsqt+RjFMlX2AvbRB/0wghap19NdBxxwGZ+h/C6DLCrcEphPIItlGrRQ=="], "vite": ["vite@6.3.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -418,14 +437,16 @@
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zod": ["zod@3.24.3", "", {}, "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg=="],
"zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="],
"@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.13.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw=="],
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
} }
} }

View File

@@ -11,18 +11,19 @@
}, },
"dependencies": { "dependencies": {
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0" "react-dom": "^19.1.0",
"highlight.js": "^11.11.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.24.0", "@eslint/js": "^9.25.1",
"@types/react": "^19.1.2", "@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2", "@types/react-dom": "^19.1.3",
"@vitejs/plugin-react": "^4.4.0", "@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.24.0", "eslint": "^9.25.1",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^6.0.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.0.0", "globals": "^16.0.0",
"react-router-dom": "^7.5.0", "react-router-dom": "^7.5.3",
"vite": "^6.3.1" "vite": "^6.3.3"
} }
} }

Some files were not shown because too many files have changed in this diff Show More