Compare commits

..

355 Commits

Author SHA1 Message Date
Ettore Di Giacinto 9a6b32f9bc chore: bump localrecall to include PostgreSQL table-name sanitization… (#478)
chore: bump localrecall to include PostgreSQL table-name sanitization fix

Pulls mudler/LocalRecall#48, which makes sanitizeTableName allowlist valid
identifier characters so collection names containing ':' (e.g. the per-user
"legacy-api-key:<agent>" namespace used by LocalAI) no longer break
PostgreSQL CREATE TABLE with "syntax error at or near ':'".

Ref: mudler/LocalAI#10375

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-06-18 16:48:34 +02:00
Ettore Di Giacinto 14aed1ae43 chore: bump localrecall to index-backed hybrid search (#477)
chore: bump localrecall to index-backed RRF hybrid search

Pulls in mudler/LocalRecall#46 (merged), which rewrites the PostgreSQL
hybrid search using the canonical Reciprocal Rank Fusion pattern
(index-backed candidate retrieval + FULL OUTER JOIN + weighted RRF). Fixes
the full sequential scan that blew past the statement timeout on large
collections (mudler/LocalAI#10186).

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-06-06 09:12:51 +02:00
Bas Hulsken 22280d4c88 allow configuring LOCALAGI to use a custom base url (#469)
allow configuring LOCALAGI to use a custom base url other than the default :3000
2026-06-02 16:12:18 +02:00
Ettore Di Giacinto 37810d918a fix(collections): rehydrate in-process collections lazily after init failure (#468)
When NewInProcessBackend boots, it iterates every on-disk collection and
calls newVectorEngine to construct its engine wrapper. For the postgres
engine that constructor performs a "test embedding" probe that requires
the embedding model to be reachable. If the embedding service is briefly
unavailable at boot — e.g. the node hosting the embedding model has
temporary NATS connectivity issues — the construction fails and the
collection is silently dropped from the in-memory map. Subsequent
operations against that collection (Upload / Search / ListEntries / …)
all return "collection not found" indefinitely, even after the embedding
service comes back, because nothing ever retries.

Worse, the collection still exists on disk (its JSON sidecar) and its
data still exists in the vector DB (e.g. the per-collection
documents_col_<uuid> table in PostgreSQL). From the user's perspective
their collection has silently disappeared.

Two changes:

1. NewInProcessBackend always registers every on-disk collection in
   state.Collections — even when newVectorEngine returns nil. A nil
   entry is a placeholder meaning "known on disk, not yet loaded".

2. backendInProcess.lookup centralises the cache read for every
   operation. If the cache holds a placeholder, it retries
   newVectorEngine now, under the write lock. So as soon as the
   embedding service is reachable again, the next request to the
   collection will rehydrate it transparently. If init still fails or
   the collection isn't on disk at all, lookup returns (nil, false)
   and the operation surfaces "collection not found" as before.

The state.EnsureCollection callback used by the internal RAG provider
already handled the placeholder case (it re-inits whenever the cache
entry is missing or nil), so it needs no change.

This does not address the underlying probe-and-cache pattern in
LocalRecall's NewPersistentPostgresCollection — read-only operations
on existing collections still require an embedding probe at engine
construction. That is a separate, deeper fix worth pursuing in
LocalRecall directly.
2026-05-08 14:52:35 +02:00
Ettore Di Giacinto c1a1231793 chore(deps): bump localrecall — go-fitz cgo → go-pdfium WASM
Pulls LocalRecall@a7724fe which switches PDF extraction from
gen2brain/go-fitz to klippa-app/go-pdfium with the WebAssembly backend.
Fixes aarch64 link failures (`__isoc23_strtol` undefined reference) in
binaries that pull LocalRecall through cgo.

Pure dep bump — no LocalAGI source changes. Indirect graph picks up
go-pdfium + tetratelabs/wazero; drops gen2brain/go-fitz +
ebitengine/purego + jupiterrider/ffi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 07:47:08 +00:00
Ettore Di Giacinto facd8881b1 chore(deps): bump localrecall to v0.6.0 for go-fitz PDF extraction
LocalRecall v0.6.0 swaps the PDF text extractor from dslipak/pdf to
gen2brain/go-fitz (libmupdf bindings) and wraps the call in a 60s
goroutine timeout. Fixes upload hangs that affected adversarial PDFs
(broken xref tables, encrypted, image-only without OCR) where the
previous parser would block indefinitely.

Pure dep bump — no LocalAGI source code changes. Indirect graph picks
up gen2brain/go-fitz, ebitengine/purego, jupiterrider/ffi; drops
dslipak/pdf.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:07:19 +00:00
Ettore Di Giacinto e83bf515d0 fix(collections): handle errors from LocalRecall constructors
LocalRecall's NewPersistent{Chrome,LocalAI,Postgres}Collection now
return errors instead of os.Exit'ing on init failure. Bump the dep and
update newVectorEngine to consume the new signatures, surfacing failures
as a nil collection (the existing "engine misconfigured" code path)
instead of letting the upstream os.Exit kill the embedding process.

Without this, a transient embedding-service or PostgreSQL outage during
lazy RAG init would crash any long-running host (e.g. the LocalAI
distributed orchestrator) instead of degrading to "no RAG available".

RAGProviderFromState already handles a nil collection from
EnsureCollection by returning (nil, nil, false) to the agent pool, which
is the same path agents take when no RAG is configured — so callers
upstream need no further change.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-05-04 16:51:00 +00:00
Ettore Di Giacinto 3369136c73 chore(deps): bump localrecall for vector-dim migration fix
Picks up the postgres backend fix where switching the embedding model
on an existing collection now resizes the VECTOR column instead of
failing every subsequent insert with SQLSTATE 22000.
2026-04-15 16:51:42 +00:00
Richard Palethorpe c65d62e762 chore: Update fiber to v2.52.11 (#461)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-03-31 19:38:55 +02:00
Ettore Di Giacinto 736ccbb95e forward reasoning
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-25 17:28:45 +00:00
Ettore Di Giacinto 640d97b6db small fixups
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-25 15:38:18 +00:00
Ettore Di Giacinto 23344560f0 small fixups
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-25 14:25:29 +00:00
Ettore Di Giacinto 3d0af0088e split run from start
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-25 13:48:23 +00:00
Ettore Di Giacinto 1b87514b2f allow to ask directly
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-25 08:33:17 +00:00
Ettore Di Giacinto b485b77037 Adapt client too
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-21 00:47:23 +00:00
Ettore Di Giacinto 07caa0b95d Adapt client too
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-21 00:46:41 +00:00
Ettore Di Giacinto 43c65ec7e8 feat: update localrecall to support files with same names in the collections
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-19 17:45:13 +00:00
dependabot[bot] cc2d2838ca chore(deps-dev): bump eslint-plugin-react-refresh from 0.5.0 to 0.5.2 in /webui/react-ui (#430)
chore(deps-dev): bump eslint-plugin-react-refresh in /webui/react-ui

Bumps [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) from 0.5.0 to 0.5.2.
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.0...v0.5.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.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>
2026-03-18 08:43:54 +01:00
Ettore Di Giacinto da286065e1 feat: support streaming mode for tool calls
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-15 22:34:07 +00:00
Ettore Di Giacinto 9438d39f70 feat: support streaming mode for tool calls
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-15 22:33:51 +00:00
Ettore Di Giacinto e38f13ab8c feat: add handler to serve raw files
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-14 22:28:28 +00:00
LocalAI [bot] 2d2da7df95 feat: add --prompt flag for foreground agent mode (#454)
* feat: add --prompt flag for foreground agent mode

- Add --prompt/-p flag to 'agent run' command
- When --prompt is provided, runs agent in foreground mode
- Creates agent, executes Ask() with the prompt, prints response, and exits
- Supports both agent name and --config file input modes
- Follows existing code patterns in the repository

* fix: correct JobResult field access for error and response

---------

Co-authored-by: localai-bot <localai-bot@noreply.github.com>
2026-03-11 17:53:47 +01:00
LocalAI [bot] dc21ee83bc feat: implement 'agent run' CLI command (#448) (#449)
* feat: implement 'agent run' CLI command (#448)

- Add 'local-agi agent run' command supporting agent name or config file
- Support 'local-agi agent run <name>' to run agent from registry (pool.json)
- Support 'local-agi agent run --config <file.json>' to run from JSON config
- Extract web server into 'local-agi serve' subcommand
- Implement agent name lookup from registry
- Add JSON config file parsing and validation
- Create standalone agent execution logic
- Add proper error handling for invalid inputs
- Reuse existing service factories (actions, connectors, filters, skills)
- Environment variable fallback for config values
- No web server - agent runs in foreground with clean SIGINT/SIGTERM handling

References: https://github.com/mudler/LocalAGI/issues/448

* refactor(cmd): use pool to start agent instead of duplicating logic

Replace ~220 lines of duplicated agent initialization code in
startStandaloneAgent() with a call to pool.StartAgentStandalone().
The new pool method delegates to the existing startAgentWithConfig(),
eliminating code duplication between the CLI and the pool.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add default help command to root

* fix: default to serve command when no subcommand provided

The e2e tests fail because when the container starts with no arguments,
the root command shows help instead of starting the web server.

Changed the root command to call serveCmd.RunE() directly when no
subcommand is provided, ensuring the web server starts by default.

This fixes the CI e2e test failure where the web server wasn't starting.

* fix: add serve subcommand to Dockerfile ENTRYPOINT

* refactor: remove unused GetRAGProvider function

* refactor: centralize env var setup in cmd/env.go

---------

Co-authored-by: localai-bot <localai-bot@noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 22:09:02 +01:00
dependabot[bot] 9b744d2bac chore(deps-dev): bump eslint from 10.0.0 to 10.0.3 in /webui/react-ui (#451)
Bumps [eslint](https://github.com/eslint/eslint) from 10.0.0 to 10.0.3.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.0.0...v10.0.3)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.0.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>
2026-03-09 23:05:19 +01:00
LocalAI [bot] 4177479f82 chore(deps): bump cogito from v0.9.2 to v0.9.3 (#452)
Co-authored-by: LocalAI Bot <localai-bot@example.com>
2026-03-09 22:37:10 +01:00
LocalAI [bot] 34db83dc32 fix(slack): correct thread timestamp and improve file upload handling (#447)
Root cause: uploadJobResultFiles was passing msgTs (placeholder reply timestamp)
as thread_ts for file uploads. Slack's API rejects this - it requires the
parent thread timestamp, not a reply's timestamp.

Changes:
1. Use 'ts' (thread root timestamp) instead of 'msgTs' in replyToUpdateMessage
2. Fix type handling in attachmentsFromMetadataOnly to handle []interface{}
3. Download and upload generated images (e.g., DALL-E) as files instead of
   just adding link attachments, to preserve temporary URLs
4. Remove dead code: generateAttachmentsFromJobResponse was never called

This matches Telegram connector behavior where files are properly uploaded
rather than just referenced by URL.

Co-authored-by: localai-bot <localai-bot@noreply.github.com>
2026-03-08 15:59:37 +01:00
Ettore Di Giacinto 5a27c471ca chore: refactoring to make it importable
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-06 16:49:48 +01:00
Ettore Di Giacinto 3ecbbf3eac chore: minor enhancement to be fully importable
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-06 16:39:30 +01:00
Ettore Di Giacinto d9bf193457 feat(ui): allow to edit agent before importing (#444)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-04 18:39:11 +01:00
Ettore Di Giacinto 2acdc6688c fix(conv): merge system messages
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-27 11:08:16 +01:00
LocalAI [bot] fb20d12ae1 feat: add automatic compaction settings to agent config (#437)
* Merge origin/main and resolve conflicts: keep all features including auto-compaction and evaluation

* fix: resolve merge conflict syntax errors in config.go

* feat: wire auto-compaction options to cogito

Wire enableAutoCompaction and autoCompactionThreshold options to
cogito.WithCompactionThreshold() when starting the agent.

---------

Co-authored-by: Team Coding Agent 1 <team-coding-agent-1@local>
2026-02-26 21:32:27 +01:00
Ettore Di Giacinto 7bbec7b6dd fix(templates): add agentname in prompt templates (#438)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-26 15:43:13 +01:00
LocalAI [bot] 905ffcb185 chore: update cogito go dependency to latest main (#436)
Updated github.com/mudler/cogito from
v0.9.2-0.20260223101954-070948df04f7 to
v0.9.2-0.20260225234859-b76691637703

Co-authored-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2026-02-26 01:12:12 +01:00
LocalAI [bot] cb82322df2 feat: add inner monologue template support for scheduler and template-based skills prompt (#435)
* feat: add scheduler task template and template-based skills prompt

- Add WithSchedulerTaskTemplate option for recurring tasks run by scheduler
- Expose scheduler_task_template in core/state/config.go for user configuration
- Add WithSkillPromptTemplate option for custom skill prompt templates
- Use {{.Skills}} slice in templates to iterate over Skill.Name, Skill.Description
- Default template mimics current XML behavior with <available_skills> format
- Rename customIntro to customTemplate in services/skills for consistency

* cleanups

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

* Update core/state/config.go

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 18:44:49 +01:00
LocalAI [bot] 56ecd1921e chore: disable reasoning by default (#433)
Set EnableReasoning DefaultValue to false in agent config form metadata.

Signed-off-by: mudler <mudler@localai.io>
2026-02-24 21:50:55 +01:00
Ettore Di Giacinto a0faa14ffe fix(slack/files): trying to debug files upload
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-23 22:39:23 +00:00
Ettore Di Giacinto 7494a1559e chore(observables): add missing observables
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-23 21:09:01 +00:00
Ettore Di Giacinto 3dad5acf1f chore(pdf): refactor and try to always send pdf
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-23 20:53:02 +00:00
Ettore Di Giacinto 99f7e3ab60 fix: finish the job once
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-23 19:10:29 +01:00
Ettore Di Giacinto 5b90d4c3e2 chore: stop tool call when model calls send_message
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-23 19:06:31 +01:00
Ettore Di Giacinto 9406d699cc chore: allow to configure inner monologue template
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-23 18:43:25 +01:00
Ettore Di Giacinto 78defbad32 fix: avoid panics
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-23 15:31:04 +01:00
Ettore Di Giacinto 2efadbf6c4 bump go mod
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-23 11:55:42 +01:00
Ettore Di Giacinto b46c300307 chore: enable more behavioral settings
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-23 11:42:30 +01:00
Ettore Di Giacinto d2407105a4 fix: remove the duplicated unlock call
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-22 23:24:30 +00:00
Ettore Di Giacinto 32a1a59389 feat: add accumulator to print tool status/results during execution (#427)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-22 22:44:19 +01:00
Ettore Di Giacinto 61a89aaf58 feat(pdf): improve pdf markdown rendering (#426)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-22 22:42:36 +01:00
Ettore Di Giacinto c5e3df41ee fix: atomic writes to the pool, create backups
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-22 19:11:46 +01:00
Ettore Di Giacinto cb37f80724 fix(slack): process all metadata
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-22 10:12:13 +01:00
Ettore Di Giacinto bc256fbbe2 chore: avoid panics from Slack connector
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-22 09:49:03 +01:00
Ettore Di Giacinto 8014d22588 fix: unify api keys for collection endpoint
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-21 23:39:19 +00:00
Ettore Di Giacinto 410ba7a467 feat: allow to manage external localrecall instances (#425)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-22 00:02:12 +01:00
Ettore Di Giacinto c56cc43552 feat(memory): add knowledgebase and memory management from LocalRecall (#424)
* feat: integrate knowledge base management

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

* refactorings

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-21 23:17:33 +01:00
Ettore Di Giacinto bc567ef7dd feat(skills): add skills management (#423)
* feat(skills): add skills management

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

* improve ui

* Update webui/skills_handlers.go

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

* Update webui/skills_handlers.go

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

* Update webui/skills_handlers.go

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

* Update webui/skills_handlers.go

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

* Update Dockerfile.webui

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

* Update go.mod

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

* Update webui/skills_handlers.go

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

* Address feedback from review

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

* Fixups

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

* allow to customize skill prompt

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

* Fixups

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-21 22:21:33 +01:00
Ettore Di Giacinto f29e98bccb fix(reasoning): do not update if no new content
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-21 15:15:57 +01:00
Ettore Di Giacinto 73d8304e2e fix: do not imply there is an action
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-20 22:44:45 +00:00
Ettore Di Giacinto 52e8df6599 fix(slack): another attempt to fix markdown result display (#422)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-20 23:15:02 +01:00
Ettore Di Giacinto 3828dd7ccc chore(deps): bump cogito
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-20 22:07:33 +00:00
Ettore Di Giacinto cea2d9d618 chore: hook reasoning callback to status updates
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-20 21:51:00 +00:00
Ettore Di Giacinto 5daf4bda8e fix: do not let agents override each other configuration
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-20 10:08:16 +01:00
Ettore Di Giacinto 508a307e8b chore: update cogito
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-20 10:02:21 +01:00
Ettore Di Giacinto 8a16ad5407 feat: bump cogito, use LocalAILLM
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-19 21:25:05 +00:00
Ettore Di Giacinto 27cc58a25f feat(ui): left navbar, dark/light theme (#421)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-17 23:21:34 +01:00
Ettore Di Giacinto a8decbcc85 chore: fix login
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-17 17:31:12 +01:00
Ettore Di Giacinto 1c083abc60 Add back old webui (login)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-17 16:18:36 +01:00
Ettore Di Giacinto 8f64d246d8 chore: bump cogito
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-17 16:05:16 +01:00
Ettore Di Giacinto 700047c6b9 chore: allow to set force reasoning tool
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-17 13:13:41 +01:00
dependabot[bot] 419a4363c3 chore(deps-dev): bump @types/react from 19.2.13 to 19.2.14 in /webui/react-ui (#420)
chore(deps-dev): bump @types/react in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.2.14
  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>
2026-02-16 22:35:14 +01:00
Ettore Di Giacinto 292d0c9c19 chore: improvements to sink state handling
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-16 19:29:03 +01:00
Ettore Di Giacinto 1147e02844 chore: bump cogito
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-16 17:31:49 +01:00
Ettore Di Giacinto c8e83dc4b9 chore: bump cogito
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-16 15:45:19 +01:00
Ettore Di Giacinto 8b5188c35b chore(cogito): bump
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-15 23:08:10 +00:00
Ettore Di Giacinto 55b631bc51 fix: append user actions
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-15 22:56:44 +01:00
Ettore Di Giacinto c1e2eeb8af chore(cogito): bump
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-15 21:34:59 +00:00
Ettore Di Giacinto 50f168f322 drop unused code, run compaction immediately at start
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-14 23:08:02 +01:00
Ettore Di Giacinto b5dacb0e4f chore: allow to choose how we store things in the vector database (#417)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-14 23:06:49 +01:00
Ettore Di Giacinto 0762d6fabb bump cogito (#416)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-14 21:25:43 +01:00
Ettore Di Giacinto 6dc515bae5 feat: expose further cogito settings
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-14 19:15:55 +01:00
Ettore Di Giacinto 9110e9cfdb fix: set sink state tool with a custom name
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-10 21:50:15 +00:00
Ettore Di Giacinto 9965e3df36 Comment out DisableSinkState in agent configuration
Comment out the DisableSinkState option in agent.go.
2026-02-10 22:43:26 +01:00
Ettore Di Giacinto da9e8d7c20 feat: add support to medatata to messages sent from the agents (#415)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-10 18:18:53 +01:00
dependabot[bot] 7f2e52c0c8 chore(deps-dev): bump @vitejs/plugin-react from 5.1.2 to 5.1.3 in /webui/react-ui (#402)
chore(deps-dev): bump @vitejs/plugin-react in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.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>
2026-02-10 16:45:09 +01:00
dependabot[bot] 6a33484fce chore(deps-dev): bump @eslint/js from 9.39.2 to 10.0.1 in /webui/react-ui (#414)
chore(deps-dev): bump @eslint/js in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 10.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:32:36 +01:00
dependabot[bot] bdf12e4c3d chore(deps-dev): bump @types/react from 19.2.10 to 19.2.13 in /webui/react-ui (#413)
chore(deps-dev): bump @types/react in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.2.13
  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>
2026-02-10 13:03:02 +01:00
dependabot[bot] 5743d6f757 chore(deps-dev): bump eslint from 9.39.2 to 10.0.0 in /webui/react-ui (#412)
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.2 to 10.0.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.2...v10.0.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.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>
2026-02-10 13:02:50 +01:00
dependabot[bot] 0845e9dc81 chore(deps-dev): bump eslint-plugin-react-refresh from 0.4.26 to 0.5.0 in /webui/react-ui (#401)
chore(deps-dev): bump eslint-plugin-react-refresh in /webui/react-ui

Bumps [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) from 0.4.26 to 0.5.0.
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.4.26...v0.5.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 09:07:40 +01:00
Ettore Di Giacinto ef2a6e2296 chore: bump LocalRecall
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-09 21:26:57 +00:00
Ettore Di Giacinto 272c54f851 fix: int type passing from react
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-09 20:49:37 +00:00
Ettore Di Giacinto 775b1a9ab8 chore: drop agents avatars (#411)
This functionality makes LocalAGI harder to starts without no real gain.
While having agents avatars is nice, on generating a new agent is
required a new image to be created, which, on small HW could become a
huge computational strain.

It moves the agent list view to tabular, and cleanups the index page to
improve readability.

We can go back at this, but the motivations are mainly:

- Do not waste computation and be more lightweight
- Less models to download unless the user wants to download image models
- Let's not make look the application "weird" as images could be
  generated which are not good looking at all

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-09 18:59:52 +01:00
Ettore Di Giacinto 673909918a feat: improve theme to follow LocalAI (#410)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-09 15:46:01 +01:00
Ettore Di Giacinto 807b7e756b Small fixups
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-09 11:22:43 +01:00
Ettore Di Giacinto c75da2235d Rename actions from knowledge base to memory 2026-02-09 00:41:27 +01:00
Ettore Di Giacinto 80a12f6662 Rename reminder actions to task actions 2026-02-09 00:33:40 +01:00
Ettore Di Giacinto 1128615140 Rename ConversationActionName to 'send_message' 2026-02-09 00:32:28 +01:00
Ettore Di Giacinto 8847bfe0fc chore: allow to set scheduler poll interval
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-08 23:26:27 +01:00
Ettore Di Giacinto 25ce5475de chore: drop deprecated set reminders
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-08 23:03:03 +01:00
Ettore Di Giacinto 2a4bc0eb0a chore: mark scheduled jobs so we inject tools to initiate new conversations
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-08 22:56:25 +01:00
Ettore Di Giacinto d0bfa01494 Change runner to arc-runner-localagent 2026-02-08 22:34:31 +01:00
Copilot 57023d6386 Integrate task scheduler into agent reminder system (#408)
* Initial plan

* Add task scheduler with cron/interval/once support and comprehensive tests

Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>

* Add comprehensive documentation for scheduler package

Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>

* Add working example demonstrating scheduler usage

Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>

* Update .gitignore to exclude example binaries and task files

Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>

* Integrate task scheduler into existing reminder system

Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>

* Fix scheduler integration and compilation errors

Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>

* Remove standalone scheduler example - integrated into agent system

Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>

* Update scheduler README to reflect integration with agent system

Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>

* replace old reminders logic

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

* A task scheduler for each agent

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

* fixups

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

* tests

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

* fixups

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

* change to duration

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-08 22:19:22 +01:00
Copilot 1c3e9b36e7 Add generate_pdf action with connector support (#409)
* Initial plan

* Add PDF generation action with tests and connector support

Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>

* Address code review feedback: Add error logging and clarify filename description

Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>

* Add path traversal protection for filename parameter

Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-08 21:09:04 +01:00
Ettore Di Giacinto 4a3ee02081 chore: cleanup
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-07 09:00:12 +01:00
Ettore Di Giacinto d804ef66e5 fix: register actions
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-06 23:21:38 +01:00
Ettore Di Giacinto cea3b7b111 chore: drop LocalOperator actions, we will use MCP for this (#407)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-06 21:51:54 +01:00
Ettore Di Giacinto 1d574069f2 feat: allow to adjust knowledgebase usage (#406)
This allows to configure specifically how the Knowledge base should be
accessible to the agent.

- Auto search: When enabled, every message will trigger a search to the
  knowledge base. This is useful mostly if you mean to give immediate
  context to the agent. Particularly suited for search agents.
- As tools: it will inject search and add tools to the knowledge base so
  the agent can handle these autonomously.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-06 21:36:44 +01:00
dependabot[bot] b09da13111 chore(deps-dev): bump globals from 17.1.0 to 17.3.0 in /webui/react-ui (#403)
Bumps [globals](https://github.com/sindresorhus/globals) from 17.1.0 to 17.3.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.1.0...v17.3.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-06 21:05:16 +01:00
Ettore Di Giacinto ba2eafe4b2 fix: pass-by state result
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-06 16:50:33 +01:00
Ettore Di Giacinto cbe4cd62ca fix: correct API url
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-05 22:49:18 +01:00
Ettore Di Giacinto 4f4ad5069d feat(gen-song): add song generation action (#404)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-05 19:00:50 +01:00
Ettore Di Giacinto 0950791be6 fix: skip KB if not enabled
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-05 10:46:35 +01:00
Ettore Di Giacinto f47d5d0e01 fix(ui): checkbox event propagation
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-03 12:25:57 +01:00
Ettore Di Giacinto e3d4ac3d0b Enhance LocalAGI description in README
Expanded description of LocalAGI to include agent creation and skillserver integration.
2026-02-01 18:30:48 +01:00
Ettore Di Giacinto c98178d41d chore(settings): move mcp config to the MCP section and rework form (#400)
* chore: move MCP configuration to the MCP section

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

* add MCP stdio form, visually split the two

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-01 18:23:41 +01:00
Ettore Di Giacinto 4e89c8aac0 chore: attach observables for standalone jobs (#399)
* chore: add observable for standalone jobs

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

* chore: update docker compose with latest models

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-01 09:36:24 +01:00
Ettore Di Giacinto c3bd2bee42 feat: bump cogito, allow to enable guidelines
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-01-31 09:41:14 +01:00
Ettore Di Giacinto 9f358abb54 chore(memory): open index only once
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-01-31 09:25:17 +01:00
Ettore Di Giacinto 456c32c284 use volumes
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-01-31 09:19:54 +01:00
Ettore Di Giacinto b527cbc332 Revert "chore: do not wait for agents to start, add timeout on MCP connections"
This reverts commit 7cc0c3e85b.
2026-01-31 09:19:26 +01:00
Ettore Di Giacinto 7cc0c3e85b chore: do not wait for agents to start, add timeout on MCP connections
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-01-30 20:20:46 +00:00
Ettore Di Giacinto 73ce451c13 feat: add automatic compaction of knowledge base (#398)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-01-30 18:00:06 +01:00
vugenti 72a7807a81 HTTP MCP Fixes (#395)
Add support for Streamable MCP over HTTP - fallback on legacy SSE transport. Fix: Prevent HTTP MCP servers from being closed immediately after initialization.
2026-01-30 15:43:34 +01:00
Ettore Di Giacinto 16775f356a Use bleve for indexing short-term memory (#397)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-01-30 15:41:23 +01:00
dependabot[bot] a363682b39 chore(deps-dev): bump vite from 7.3.0 to 7.3.1 in /webui/react-ui (#387)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.0 to 7.3.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.1/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.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>
2026-01-29 10:33:37 +01:00
Ettore Di Giacinto 2288e7f08d feat: update localrecall and use postgresql as default (#394)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-01-29 10:26:25 +01:00
dependabot[bot] c703495e8c chore(deps-dev): bump react-router-dom from 7.11.0 to 7.13.0 in /webui/react-ui (#392)
chore(deps-dev): bump react-router-dom in /webui/react-ui

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-29 09:40:41 +01:00
dependabot[bot] 73c1158aa2 chore(deps): bump react and @types/react in /webui/react-ui (#393)
Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react). These dependencies needed to be updated together.

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

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

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: "@types/react"
  dependency-version: 19.2.9
  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>
2026-01-29 09:40:30 +01:00
dependabot[bot] a1036a166b chore(deps): bump react-dom from 19.2.3 to 19.2.4 in /webui/react-ui (#391)
Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) from 19.2.3 to 19.2.4.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.4/packages/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  dependency-version: 19.2.4
  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>
2026-01-28 11:40:05 +01:00
dependabot[bot] 9a669e2552 chore(deps-dev): bump globals from 16.5.0 to 17.1.0 in /webui/react-ui (#390)
Bumps [globals](https://github.com/sindresorhus/globals) from 16.5.0 to 17.1.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.5.0...v17.1.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.1.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>
2026-01-28 11:39:52 +01:00
Ettore Di Giacinto cfdefcd075 chore(knowledgebase): clean collection names (no spaces,no uppercase) (#385)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-01-12 23:15:19 +01:00
dependabot[bot] ec3f302cdf chore(deps-dev): bump react-router-dom from 7.10.1 to 7.11.0 in /webui/react-ui (#383)
chore(deps-dev): bump react-router-dom in /webui/react-ui

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-23 09:07:57 +01:00
dependabot[bot] 345dd672d2 chore(deps-dev): bump eslint-plugin-react-refresh from 0.4.25 to 0.4.26 in /webui/react-ui (#382)
chore(deps-dev): bump eslint-plugin-react-refresh in /webui/react-ui

Bumps [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) from 0.4.25 to 0.4.26.
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.4.25...v0.4.26)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.4.26
  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-12-23 09:07:42 +01:00
dependabot[bot] 7e50afde73 chore(deps-dev): bump eslint from 9.39.1 to 9.39.2 in /webui/react-ui (#381)
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.1 to 9.39.2.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.1...v9.39.2)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.39.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-12-23 09:07:29 +01:00
dependabot[bot] e33046db03 chore(deps): bump react-dom from 19.2.1 to 19.2.3 in /webui/react-ui (#375)
Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) from 19.2.1 to 19.2.3.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.3/packages/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  dependency-version: 19.2.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-12-21 21:27:10 +01:00
Ettore Di Giacinto 9a33537ae0 chore: move logging to its own package so can be shared across repos (#380)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-12-19 23:05:04 +01:00
dependabot[bot] c7040585d3 chore(deps-dev): bump eslint-plugin-react-refresh from 0.4.24 to 0.4.25 in /webui/react-ui (#378)
chore(deps-dev): bump eslint-plugin-react-refresh in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.4.25
  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-12-19 22:26:22 +01:00
dependabot[bot] 85823961f3 chore(deps-dev): bump @eslint/js from 9.39.1 to 9.39.2 in /webui/react-ui (#377)
chore(deps-dev): bump @eslint/js in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.39.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-12-19 22:26:04 +01:00
dependabot[bot] ef0e6d2025 chore(deps): bump react from 19.2.1 to 19.2.3 in /webui/react-ui (#376)
Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) from 19.2.1 to 19.2.3.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.3/packages/react)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.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-12-19 22:25:49 +01:00
dependabot[bot] 4687efb502 chore(deps-dev): bump vite from 7.2.7 to 7.3.0 in /webui/react-ui (#374)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.2.7 to 7.3.0.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.0/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.0/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 22:25:33 +01:00
dependabot[bot] 9829930451 chore(deps-dev): bump vite from 7.2.4 to 7.2.7 in /webui/react-ui (#373)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.2.4 to 7.2.7.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.2.7/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.2.7/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.2.7
  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-12-12 12:45:16 +01:00
dependabot[bot] eaf81438fa chore(deps): bump react-dom from 19.2.0 to 19.2.1 in /webui/react-ui (#372)
Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) from 19.2.0 to 19.2.1.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.1/packages/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  dependency-version: 19.2.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-12-12 12:45:07 +01:00
dependabot[bot] 8141defbaf chore(deps-dev): bump react-router-dom from 7.9.6 to 7.10.1 in /webui/react-ui (#371)
chore(deps-dev): bump react-router-dom in /webui/react-ui

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 21:48:50 +01:00
dependabot[bot] a1667b2227 chore(deps): bump react from 19.2.0 to 19.2.1 in /webui/react-ui (#370)
Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) from 19.2.0 to 19.2.1.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.1/packages/react)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.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-12-08 21:48:33 +01:00
dependabot[bot] 05f21c6c80 chore(deps-dev): bump @vitejs/plugin-react from 5.1.1 to 5.1.2 in /webui/react-ui (#369)
chore(deps-dev): bump @vitejs/plugin-react in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.1.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-12-08 21:48:22 +01:00
dependabot[bot] f6ab437796 chore(deps): bump docker/metadata-action from 5.7.0 to 5.10.0 (#364)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.7.0 to 5.10.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/902fa8ec7d6ecbf8d84d538b9b233a880e428804...c299e40c65443455700f0fdfc63efafe5b349051)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: 5.10.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-12-04 14:23:14 +01:00
dependabot[bot] 9405fe3825 chore(deps): bump actions/checkout from 4 to 6 (#363)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [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/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 14:23:01 +01:00
dependabot[bot] bef63357be chore(deps-dev): bump @types/react from 19.2.2 to 19.2.7 in /webui/react-ui (#360)
chore(deps-dev): bump @types/react in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.2.7
  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-12-04 14:22:37 +01:00
dependabot[bot] 0fec0d39b7 chore(deps-dev): bump vite from 7.1.12 to 7.2.4 in /webui/react-ui (#361)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.12 to 7.2.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.2.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.2.4
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 21:45:52 +01:00
dependabot[bot] 8709422e05 chore(deps-dev): bump @types/react-dom from 19.2.2 to 19.2.3 in /webui/react-ui (#359)
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.2.2 to 19.2.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.2.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-11-26 21:45:37 +01:00
dependabot[bot] 229aab41f8 chore(deps-dev): bump eslint from 9.39.0 to 9.39.1 in /webui/react-ui (#352)
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.0 to 9.39.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.0...v9.39.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.39.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-11-26 21:45:25 +01:00
dependabot[bot] 853a168773 chore(deps-dev): bump react-router-dom from 7.9.5 to 7.9.6 in /webui/react-ui (#358)
chore(deps-dev): bump react-router-dom in /webui/react-ui

Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.9.5 to 7.9.6.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.9.6/packages/react-router-dom)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-23 20:55:56 +01:00
dependabot[bot] fa963660e0 chore(deps-dev): bump @vitejs/plugin-react from 5.1.0 to 5.1.1 in /webui/react-ui (#357)
chore(deps-dev): bump @vitejs/plugin-react in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.1.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-11-23 20:55:43 +01:00
dependabot[bot] fbb098e996 chore(deps-dev): bump @eslint/js from 9.38.0 to 9.39.0 in /webui/react-ui (#342)
chore(deps-dev): bump @eslint/js in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.39.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 09:31:29 +01:00
dependabot[bot] b124244b0c chore(deps-dev): bump react-router-dom from 7.9.4 to 7.9.5 in /webui/react-ui (#341)
chore(deps-dev): bump react-router-dom in /webui/react-ui

Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.9.4 to 7.9.5.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.9.5/packages/react-router-dom)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 09:31:19 +01:00
dependabot[bot] c7d1b58340 chore(deps-dev): bump eslint from 9.38.0 to 9.39.0 in /webui/react-ui (#343)
Bumps [eslint](https://github.com/eslint/eslint) from 9.38.0 to 9.39.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.38.0...v9.39.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.39.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 22:11:37 +01:00
dependabot[bot] 9971819350 chore(deps-dev): bump globals from 16.3.0 to 16.5.0 in /webui/react-ui (#344)
Bumps [globals](https://github.com/sindresorhus/globals) from 16.3.0 to 16.5.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.3.0...v16.5.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 16.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 22:11:22 +01:00
dependabot[bot] e3769d3e1f chore(deps): bump github.com/modelcontextprotocol/go-sdk from 1.0.0 to 1.1.0 (#346)
chore(deps): bump github.com/modelcontextprotocol/go-sdk

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

---
updated-dependencies:
- dependency-name: github.com/modelcontextprotocol/go-sdk
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 22:10:56 +01:00
dependabot[bot] f777ce2992 chore(deps): bump python from 3.13-slim to 3.14-slim (#338)
Bumps python from 3.13-slim to 3.14-slim.

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

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

Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 6.0.0 to 7.0.1.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-28 11:03:56 +01:00
dependabot[bot] 4afdb6f813 chore(deps): bump github.com/tmc/langchaingo from 0.1.13 to 0.1.14 (#335)
Bumps [github.com/tmc/langchaingo](https://github.com/tmc/langchaingo) from 0.1.13 to 0.1.14.
- [Release notes](https://github.com/tmc/langchaingo/releases)
- [Commits](https://github.com/tmc/langchaingo/compare/v0.1.13...v0.1.14)

---
updated-dependencies:
- dependency-name: github.com/tmc/langchaingo
  dependency-version: 0.1.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-28 11:03:46 +01:00
dependabot[bot] c610c2c6ca chore(deps): bump github.com/gofiber/fiber/v2 from 2.52.8 to 2.52.9 (#306)
Bumps [github.com/gofiber/fiber/v2](https://github.com/gofiber/fiber) from 2.52.8 to 2.52.9.
- [Release notes](https://github.com/gofiber/fiber/releases)
- [Commits](https://github.com/gofiber/fiber/compare/v2.52.8...v2.52.9)

---
updated-dependencies:
- dependency-name: github.com/gofiber/fiber/v2
  dependency-version: 2.52.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-28 09:17:07 +01:00
dependabot[bot] 047a324e0e chore(deps-dev): bump @eslint/js from 9.37.0 to 9.38.0 in /webui/react-ui (#333)
chore(deps-dev): bump @eslint/js in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-28 09:16:45 +01:00
dependabot[bot] e37283cb11 chore(deps): bump github.com/valyala/fasthttp from 1.62.0 to 1.68.0 (#334)
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.62.0 to 1.68.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.62.0...v1.68.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-version: 1.68.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 21:34:18 +01:00
dependabot[bot] d6f10b28c1 chore(deps-dev): bump vite from 7.1.4 to 7.1.12 in /webui/react-ui (#331)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.4 to 7.1.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.1.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.12/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 21:33:35 +01:00
dependabot[bot] c082b42395 chore(deps-dev): bump @vitejs/plugin-react from 5.0.4 to 5.1.0 in /webui/react-ui (#332)
chore(deps-dev): bump @vitejs/plugin-react in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 21:33:23 +01:00
dependabot[bot] 252ee2aff6 chore(deps-dev): bump react-router-dom from 7.7.0 to 7.9.4 in /webui/react-ui (#323)
chore(deps-dev): bump react-router-dom in /webui/react-ui

Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.7.0 to 7.9.4.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.9.4/packages/react-router-dom)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 19:05:44 +01:00
dependabot[bot] fffc1f8647 chore(deps): bump react and @types/react in /webui/react-ui (#322)
Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react). These dependencies needed to be updated together.

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

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

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: "@types/react"
  dependency-version: 19.2.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 19:05:35 +01:00
dependabot[bot] dc1653294d chore(deps): bump actions/setup-go from 5 to 6 (#308)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 19:01:46 +01:00
dependabot[bot] 3862c3c0eb chore(deps-dev): bump eslint from 9.35.0 to 9.38.0 in /webui/react-ui (#326)
Bumps [eslint](https://github.com/eslint/eslint) from 9.35.0 to 9.38.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.35.0...v9.38.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 19:01:32 +01:00
dependabot[bot] 756c46b526 chore(deps-dev): bump eslint-plugin-react-refresh from 0.4.20 to 0.4.24 in /webui/react-ui (#327)
chore(deps-dev): bump eslint-plugin-react-refresh in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.4.24
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 19:01:14 +01:00
Ettore Di Giacinto 73a6be8264 feat: consume cogito for agent reasoning (#320)
* WIP

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

* Drop old webui

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

* Almost there

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

* It builds

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

* Make it build

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

* fixups, still doesn't work

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

* unused now

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

* Send result before closing

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

* Fix observability

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

* Drop MCP code and wire-up in cogito

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

* Drop some templates

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

* Keep reporting into conv

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

* tests fixups

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

* tests fixups

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

* fixups

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

* Do not complete observable during thought process

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

* Update cogito

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

* Fixups

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

* Fixups

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

* Fixups

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

* Drop unneded option now

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

* Fixups

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

* Better handling of user tools

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

* TEST

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

* Add flake attempts

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

* Revert "TEST"

This reverts commit 8b12a9fd03.

* tKeep indexing MCP actions

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

* Split CI jobs to improve speed

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

* CI optimizations

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

* Bump timeout

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

* Bump cogito

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

* fix: always commit last progress

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

* chore: better management of observables

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

---------

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

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

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

---
updated-dependencies:
- dependency-name: react-dom
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-16 18:52:24 +02:00
dependabot[bot] a0ab067bf7 chore(deps-dev): bump @vitejs/plugin-react from 5.0.2 to 5.0.4 in /webui/react-ui (#325)
chore(deps-dev): bump @vitejs/plugin-react in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-14 19:06:25 +02:00
Ettore Di Giacinto c719cbd544 Remove mcpbox service from docker-compose.nvidia.yaml (#319)
Removed mcpbox service extension from NVIDIA compose file.


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

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

* Drop MCP Box

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

---------

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

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

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.37.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 09:23:46 +02:00
Michel Weimerskirch a5a602c5b0 Add "Play" button to action configuration for testing in Playground (#315) 2025-10-07 09:23:26 +02:00
Michel Weimerskirch 500862b768 Add "clear history" option to the agent observables (#312) 2025-10-03 23:06:21 +02:00
Michel Weimerskirch b06522d1ec Add custom name, description, and payload description to WebhookAction (#311) 2025-10-03 23:05:14 +02:00
Michel Weimerskirch 0c312c9caa Update README with detailed instructions for development (#310) 2025-10-03 23:04:23 +02:00
Michel Weimerskirch d456315f19 Add webhook action for HTTP request integration (#309)
* Add Webhook action handling

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

* Enhance Webhook action documentation and runtime handling

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

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-version: 2.25.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-24 10:52:02 +02:00
dependabot[bot] 471c937d57 chore(deps): bump github.com/JohannesKaufmann/html-to-markdown/v2 from 2.3.3 to 2.4.0 (#286)
chore(deps): bump github.com/JohannesKaufmann/html-to-markdown/v2

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

---
updated-dependencies:
- dependency-name: github.com/JohannesKaufmann/html-to-markdown/v2
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-19 18:14:52 +02:00
dependabot[bot] ad61551db5 chore(deps-dev): bump @vitejs/plugin-react from 4.7.0 to 5.0.2 in /webui/react-ui (#288)
chore(deps-dev): bump @vitejs/plugin-react in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-19 18:14:36 +02:00
dependabot[bot] 2bad9c399f chore(deps-dev): bump @eslint/js from 9.33.0 to 9.35.0 in /webui/react-ui (#298)
chore(deps-dev): bump @eslint/js in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.35.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-16 15:43:21 +02:00
dependabot[bot] 580ad7d46f chore(deps): bump github.com/sashabaranov/go-openai from 1.40.3 to 1.41.2 (#296)
chore(deps): bump github.com/sashabaranov/go-openai

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-16 15:42:42 +02:00
dependabot[bot] 47f013f3e1 chore(deps-dev): bump eslint from 9.34.0 to 9.35.0 in /webui/react-ui (#299)
Bumps [eslint](https://github.com/eslint/eslint) from 9.34.0 to 9.35.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.34.0...v9.35.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.35.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-16 15:42:18 +02:00
mudler d5219a9379 Update example 2025-09-13 16:42:25 +02:00
mudler 00460b1538 fix: do not change name field
Signed-off-by: mudler <mudler@localai.io>
2025-09-13 16:33:08 +02:00
Ettore Di Giacinto 98e62edf12 feat(actions): add action to control pikvm (#294)
Signed-off-by: mudler <mudler@localai.io>
2025-09-13 16:28:42 +02:00
Ettore Di Giacinto 592969f976 feat(actions/dynamicprompt): allow to pass-by a configuration (#293)
This is useful mainly to pass-by from the web interface a generic string
that can be later re-used in the custom actions or dynamic prompt during
Init().

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

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

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.34.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-09 23:26:36 +02:00
Ettore Di Giacinto 684ce43eb8 feat: preload custom actions from dir (#290)
* feat: preload custom actions from dir

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

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

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Signed-off-by: mudler <mudler@localai.io>
2025-09-09 18:05:15 +02:00
dependabot[bot] 39d13d155f chore(deps-dev): bump vite from 7.0.6 to 7.1.4 in /webui/react-ui (#287)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.0.6 to 7.1.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.4
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-02 21:02:16 +02:00
dependabot[bot] 2d5523a129 chore(deps): bump github.com/metoro-io/mcp-golang from 0.14.0 to 0.16.0 (#279)
Bumps [github.com/metoro-io/mcp-golang](https://github.com/metoro-io/mcp-golang) from 0.14.0 to 0.16.0.
- [Release notes](https://github.com/metoro-io/mcp-golang/releases)
- [Changelog](https://github.com/metoro-io/mcp-golang/blob/main/.goreleaser.yml)
- [Commits](https://github.com/metoro-io/mcp-golang/compare/v0.14.0...v0.16.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 21:54:03 +02:00
Ettore Di Giacinto 7fe3ed2169 feat(custom actions): allow to specify a dir with all custom actions (#277)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-08-25 09:33:21 +02:00
dependabot[bot] 6db3b5f31f chore(deps-dev): bump @eslint/js from 9.31.0 to 9.33.0 in /webui/react-ui (#263)
chore(deps-dev): bump @eslint/js in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.33.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 08:41:50 +02:00
dependabot[bot] f6ac820a3f chore(deps): bump github.com/emersion/go-smtp from 0.22.0 to 0.24.0 (#273)
Bumps [github.com/emersion/go-smtp](https://github.com/emersion/go-smtp) from 0.22.0 to 0.24.0.
- [Release notes](https://github.com/emersion/go-smtp/releases)
- [Commits](https://github.com/emersion/go-smtp/compare/v0.22.0...v0.24.0)

---
updated-dependencies:
- dependency-name: github.com/emersion/go-smtp
  dependency-version: 0.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 08:41:27 +02:00
dependabot[bot] d955284278 chore(deps): bump github.com/go-telegram/bot from 1.15.0 to 1.17.0 (#270)
Bumps [github.com/go-telegram/bot](https://github.com/go-telegram/bot) from 1.15.0 to 1.17.0.
- [Release notes](https://github.com/go-telegram/bot/releases)
- [Changelog](https://github.com/go-telegram/bot/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-telegram/bot/compare/v1.15.0...v1.17.0)

---
updated-dependencies:
- dependency-name: github.com/go-telegram/bot
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 08:41:05 +02:00
dependabot[bot] 3fd6134bcd chore(deps): bump github.com/metoro-io/mcp-golang from 0.13.0 to 0.14.0 (#271)
Bumps [github.com/metoro-io/mcp-golang](https://github.com/metoro-io/mcp-golang) from 0.13.0 to 0.14.0.
- [Release notes](https://github.com/metoro-io/mcp-golang/releases)
- [Changelog](https://github.com/metoro-io/mcp-golang/blob/main/.goreleaser.yml)
- [Commits](https://github.com/metoro-io/mcp-golang/compare/v0.13.0...v0.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 08:40:44 +02:00
Ettore Di Giacinto 4d875e9e2a fix: gen memory actions only in DynamicPromptMemory (#259)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-08-07 18:55:29 +02:00
Richard Palethorpe 79b72ec311 fix(ui): Correct typos in dynamic prompts (#258)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-08-05 18:44:08 +02:00
Ettore Di Giacinto 8487459d90 feat: multimodal action and dynamic prompts results (#257)
* feat(actions): allow actions to return images

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

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

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

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

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

* chore: fixups

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-08-05 18:43:50 +02:00
Ettore Di Giacinto be336adf02 feat(dynamic-prompts): add memory tool to dynamic prompt (#256)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-08-04 16:58:11 +02:00
Ettore Di Giacinto be1acb06fd Update README.md 2025-07-25 23:04:09 +02:00
Richard Palethorpe 26ec479596 fix(ui): Avoid passing object (multimedia message) into React template
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-07-25 17:00:46 +01:00
dependabot[bot] efee3c462d chore(deps-dev): bump vite from 7.0.3 to 7.0.5 in /webui/react-ui (#246)
---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.0.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-25 11:12:20 +02:00
Ettore Di Giacinto 4cf52ec698 feat(memories): add action to handle simple memory as flat files (#252)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-07-24 23:17:05 +02:00
dependabot[bot] ba8ea0117e chore(deps-dev): bump @eslint/js from 9.30.1 to 9.31.0 in /webui/react-ui (#240)
chore(deps-dev): bump @eslint/js in /webui/react-ui

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.31.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-24 15:19:27 +02:00
dependabot[bot] 36770b70ae chore(deps-dev): bump react-router-dom from 7.6.3 to 7.7.0 in /webui/react-ui (#248)
chore(deps-dev): bump react-router-dom in /webui/react-ui

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2025-07-24 15:19:09 +02:00
dependabot[bot] 5784be2853 chore(deps-dev): bump eslint from 9.30.1 to 9.31.0 in /webui/react-ui (#241)
---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.31.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-24 08:30:58 +02:00
dependabot[bot] 5b93e5c21c chore(deps-dev): bump @vitejs/plugin-react from 4.6.0 to 4.7.0 in /webui/react-ui (#249)
chore(deps-dev): bump @vitejs/plugin-react in /webui/react-ui

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 4.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-23 22:02:27 +02:00
Ettore Di Giacinto b0a43b0cb7 fix(telegram): correctly process group chat messages
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-07-22 22:25:13 +02:00
dependabot[bot] 51bb368073 chore(deps): bump golang.org/x/crypto from 0.39.0 to 0.40.0 (#244)
---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.40.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-07-22 16:51:42 +02:00
dependabot[bot] 153fcf6cf0 chore(deps-dev): bump vite from 7.0.0 to 7.0.2 in /webui/react-ui (#234)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.0.0 to 7.0.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/v7.0.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.0.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-07-10 15:13:17 +02:00
Richard Palethorpe b52571dbea fix(api): Validate input function call result message (#239)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-07-10 15:12:59 +02:00
dependabot[bot] c0a4f1026f chore(deps-dev): bump eslint from 9.30.0 to 9.30.1 in /webui/react-ui (#233)
Bumps [eslint](https://github.com/eslint/eslint) from 9.30.0 to 9.30.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.30.0...v9.30.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.30.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-07-10 08:21:03 +02:00
dependabot[bot] aab73ead5c chore(deps-dev): bump globals from 16.2.0 to 16.3.0 in /webui/react-ui (#236)
Bumps [globals](https://github.com/sindresorhus/globals) from 16.2.0 to 16.3.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.2.0...v16.3.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 16.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-10 08:20:29 +02:00
dependabot[bot] f05af02864 chore(deps): bump github.com/slack-go/slack from 0.17.2 to 0.17.3 (#237)
Bumps [github.com/slack-go/slack](https://github.com/slack-go/slack) from 0.17.2 to 0.17.3.
- [Release notes](https://github.com/slack-go/slack/releases)
- [Changelog](https://github.com/slack-go/slack/blob/master/history.go)
- [Commits](https://github.com/slack-go/slack/compare/v0.17.2...v0.17.3)

---
updated-dependencies:
- dependency-name: github.com/slack-go/slack
  dependency-version: 0.17.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-07-10 08:20:05 +02:00
dependabot[bot] 46adfb5258 chore(deps): bump github.com/slack-go/slack from 0.17.1 to 0.17.2 (#222)
Bumps [github.com/slack-go/slack](https://github.com/slack-go/slack) from 0.17.1 to 0.17.2.
- [Release notes](https://github.com/slack-go/slack/releases)
- [Changelog](https://github.com/slack-go/slack/blob/master/history.go)
- [Commits](https://github.com/slack-go/slack/compare/v0.17.1...v0.17.2)

---
updated-dependencies:
- dependency-name: github.com/slack-go/slack
  dependency-version: 0.17.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-07-07 11:21:12 +02:00
dependabot[bot] d6784f11a7 chore(deps-dev): bump react-router-dom from 7.6.2 to 7.6.3 in /webui/react-ui (#224)
chore(deps-dev): bump react-router-dom in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.6.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-07-07 11:20:58 +02:00
dependabot[bot] 4bd86bd842 chore(deps-dev): bump @eslint/js from 9.28.0 to 9.30.0 in /webui/react-ui (#227)
chore(deps-dev): bump @eslint/js in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.30.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 11:20:40 +02:00
Richard Palethorpe e630ead9c0 fix(docker): Update model and backend directories (#232)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-07-07 11:20:20 +02:00
Richard Palethorpe 5bd97908b5 fix(observability): Show tool calls as the completion of jobs 2025-07-07 10:19:48 +01:00
Richard Palethorpe f98c306b48 chore: Add web search test
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-07-07 10:19:48 +01:00
Guillaume Muller cf60dd88c4 Add docker compose for amd gpu (#230)
add docker compose for amd gpu
2025-07-02 23:15:13 +02:00
dependabot[bot] dc7badd034 chore(deps-dev): bump vite from 6.3.5 to 7.0.0 in /webui/react-ui (#226)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 7.0.0.
- [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/create-vite@7.0.0/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.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-07-02 09:30:58 +02:00
dependabot[bot] 20511283b5 chore(deps-dev): bump eslint from 9.29.0 to 9.30.0 in /webui/react-ui (#228)
Bumps [eslint](https://github.com/eslint/eslint) from 9.29.0 to 9.30.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.29.0...v9.30.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.30.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 09:30:37 +02:00
dependabot[bot] 5c233c8aa5 chore(deps): bump github.com/sashabaranov/go-openai from 1.40.1 to 1.40.3 (#225)
chore(deps): bump github.com/sashabaranov/go-openai

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

---
updated-dependencies:
- dependency-name: github.com/sashabaranov/go-openai
  dependency-version: 1.40.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-07-02 09:30:19 +02:00
Ettore Di Giacinto 9500ec7af0 feat(mutlimodal): do parse all images shared in the conversation (#221)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-06-30 22:56:30 +02:00
Ettore Di Giacinto 1fb7f8bc75 Update LocalAI images
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-06-28 18:31:42 +02:00
Ettore Di Giacinto 62723267a9 Add final remarks 2025-06-28 17:42:11 +02:00
Ettore Di Giacinto e4da4b307c Update README with docs on how to create custom actions
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-06-28 17:40:08 +02:00
dependabot[bot] 578de58e44 chore(deps-dev): bump eslint from 9.28.0 to 9.29.0 in /webui/react-ui (#211)
Bumps [eslint](https://github.com/eslint/eslint) from 9.28.0 to 9.29.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.28.0...v9.29.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.29.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-25 21:46:26 +02:00
dependabot[bot] aff460c7c2 chore(deps-dev): bump @types/react from 19.1.6 to 19.1.8 in /webui/react-ui (#213)
chore(deps-dev): bump @types/react in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.8
  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-06-25 21:46:14 +02:00
Richard Palethorpe ff6890c9c1 feat(api): Handle tool calls in responses API
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-06-24 22:01:50 +01:00
Ettore Di Giacinto 9160ca598e fix(multimodal): fix multimodal input in telegram and slack (#219)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-06-24 23:00:45 +02:00
dependabot[bot] 7a8b3d93cf chore(deps-dev): bump @vitejs/plugin-react from 4.5.2 to 4.6.0 in /webui/react-ui (#217)
chore(deps-dev): bump @vitejs/plugin-react in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 4.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-24 09:40:40 +02:00
Alixander 86659e704a Fix/catch nil panic (#216)
* fix: use correct env key in panic

* fix: prevent panic, catch nil pointer
2025-06-23 18:32:37 +02:00
Alixander 6e999927ec fix: use correct env key in panic (#215) 2025-06-23 18:31:18 +02:00
dependabot[bot] 863c6f3dcb chore(deps-dev): bump @vitejs/plugin-react from 4.5.0 to 4.5.1 in /webui/react-ui (#206)
chore(deps-dev): bump @vitejs/plugin-react in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 4.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-06-10 17:04:20 +00:00
dependabot[bot] 2a6378764b chore(deps): bump github.com/sashabaranov/go-openai from 1.40.0 to 1.40.1 (#196)
chore(deps): bump github.com/sashabaranov/go-openai

Bumps [github.com/sashabaranov/go-openai](https://github.com/sashabaranov/go-openai) from 1.40.0 to 1.40.1.
- [Release notes](https://github.com/sashabaranov/go-openai/releases)
- [Commits](https://github.com/sashabaranov/go-openai/compare/v1.40.0...v1.40.1)

---
updated-dependencies:
- dependency-name: github.com/sashabaranov/go-openai
  dependency-version: 1.40.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-06-10 16:41:11 +00:00
dependabot[bot] da7cb91f83 chore(deps-dev): bump @types/react-dom from 19.1.5 to 19.1.6 in /webui/react-ui (#205)
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.5 to 19.1.6.
- [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.6
  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-06-10 18:34:49 +02:00
Richard Palethorpe 13e0f12c1b fix(api): Use RawMessage for nested polymorphic JSON in OpenAI request input (#207)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-06-10 17:50:49 +02:00
dependabot[bot] a0a9299cda chore(deps-dev): bump react-router-dom from 7.6.1 to 7.6.2 in /webui/react-ui (#204)
chore(deps-dev): bump react-router-dom in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.6.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-06-10 17:48:58 +02:00
dependabot[bot] 15237d3137 chore(deps): bump golang.org/x/crypto from 0.38.0 to 0.39.0 (#203)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.38.0 to 0.39.0.
- [Commits](https://github.com/golang/crypto/compare/v0.38.0...v0.39.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.39.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-06-10 17:48:41 +02:00
dependabot[bot] e97cd43cdb chore(deps): bump github.com/slack-go/slack from 0.16.0 to 0.17.1 (#202)
Bumps [github.com/slack-go/slack](https://github.com/slack-go/slack) from 0.16.0 to 0.17.1.
- [Release notes](https://github.com/slack-go/slack/releases)
- [Changelog](https://github.com/slack-go/slack/blob/master/history.go)
- [Commits](https://github.com/slack-go/slack/compare/v0.16.0...v0.17.1)

---
updated-dependencies:
- dependency-name: github.com/slack-go/slack
  dependency-version: 0.17.1
  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-06-10 17:48:12 +02:00
dependabot[bot] 1313f805ae chore(deps-dev): bump @types/react from 19.1.4 to 19.1.6 in /webui/react-ui (#201)
chore(deps-dev): bump @types/react in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.6
  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-06-10 17:47:55 +02:00
dependabot[bot] aafbd9159a chore(deps-dev): bump @eslint/js from 9.27.0 to 9.28.0 in /webui/react-ui (#200)
chore(deps-dev): bump @eslint/js in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.28.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-04 09:25:22 +02:00
dependabot[bot] cff90bc709 chore(deps): bump github.com/JohannesKaufmann/html-to-markdown/v2 from 2.3.2 to 2.3.3 (#199)
chore(deps): bump github.com/JohannesKaufmann/html-to-markdown/v2

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

---
updated-dependencies:
- dependency-name: github.com/JohannesKaufmann/html-to-markdown/v2
  dependency-version: 2.3.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-06-03 08:22:02 +02:00
dependabot[bot] e2de768e09 chore(deps-dev): bump eslint from 9.27.0 to 9.28.0 in /webui/react-ui (#198)
Bumps [eslint](https://github.com/eslint/eslint) from 9.27.0 to 9.28.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.27.0...v9.28.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.28.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-03 08:21:41 +02:00
dependabot[bot] 80871db0de chore(deps): bump github.com/bwmarrin/discordgo from 0.28.1 to 0.29.0 (#197)
Bumps [github.com/bwmarrin/discordgo](https://github.com/bwmarrin/discordgo) from 0.28.1 to 0.29.0.
- [Release notes](https://github.com/bwmarrin/discordgo/releases)
- [Commits](https://github.com/bwmarrin/discordgo/compare/v0.28.1...v0.29.0)

---
updated-dependencies:
- dependency-name: github.com/bwmarrin/discordgo
  dependency-version: 0.29.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-06-03 08:21:15 +02:00
Ettore Di Giacinto 50cad776aa feat: do not use JSON extraction for reasoning (#194)
Signed-off-by: mudler <mudler@localai.io>
2025-06-02 10:29:19 +02:00
Ettore Di Giacinto 56b6f7240c feat: improve parameter generation by forcing reasoning (#193)
* feat: improve parameter generation by forcing reasoning

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

* Update core/agent/actions.go

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

* Update core/agent/actions.go

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

* Try to change default models

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-06-01 10:11:04 +02:00
dependabot[bot] f0dac5ca22 chore(deps-dev): bump globals from 16.0.0 to 16.2.0 in /webui/react-ui (#185)
Bumps [globals](https://github.com/sindresorhus/globals) from 16.0.0 to 16.2.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.0.0...v16.2.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 16.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-01 09:23:24 +02:00
dependabot[bot] c9bc5808c2 chore(deps): bump github.com/sashabaranov/go-openai from 1.39.1 to 1.40.0 (#170)
chore(deps): bump github.com/sashabaranov/go-openai

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

---
updated-dependencies:
- dependency-name: github.com/sashabaranov/go-openai
  dependency-version: 1.40.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-06-01 09:23:07 +02:00
dependabot[bot] 037aab3bdd chore(deps-dev): bump @vitejs/plugin-react from 4.4.1 to 4.5.0 in /webui/react-ui (#184)
chore(deps-dev): bump @vitejs/plugin-react in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 4.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-28 22:03:02 +02:00
dependabot[bot] e2679fae5c chore(deps-dev): bump react-router-dom from 7.6.0 to 7.6.1 in /webui/react-ui (#186)
chore(deps-dev): bump react-router-dom in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.6.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-05-28 22:02:47 +02:00
dependabot[bot] 4a0ff17990 chore(deps): bump github.com/metoro-io/mcp-golang from 0.12.0 to 0.13.0 (#189)
Bumps [github.com/metoro-io/mcp-golang](https://github.com/metoro-io/mcp-golang) from 0.12.0 to 0.13.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.12.0...v0.13.0)

---
updated-dependencies:
- dependency-name: github.com/metoro-io/mcp-golang
  dependency-version: 0.13.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-28 22:02:31 +02:00
dependabot[bot] 5af97a5dd3 chore(deps): bump github.com/gofiber/fiber/v2 from 2.52.6 to 2.52.8 (#190)
Bumps [github.com/gofiber/fiber/v2](https://github.com/gofiber/fiber) from 2.52.6 to 2.52.8.
- [Release notes](https://github.com/gofiber/fiber/releases)
- [Commits](https://github.com/gofiber/fiber/compare/v2.52.6...v2.52.8)

---
updated-dependencies:
- dependency-name: github.com/gofiber/fiber/v2
  dependency-version: 2.52.8
  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-28 22:02:17 +02:00
Ettore Di Giacinto 37aa532cc2 feat(telegram): Add support for groups (#183)
Add support for groups

Signed-off-by: mudler <mudler@localai.io>
2025-05-26 09:40:41 +02:00
Ettore Di Giacinto 9a90153dc6 feat(reminders): add reminder system to perform long-term goals in the background (#176)
* feat(reminders): add self-ability to set reminders

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

* feat(reminders): surface reminders result to the user as new conversations

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

* Fixups

* Subscribe all connectors to agents new messages

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

* Set reminders in the list

* fix(telegram): do not always auth

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

* Small fixups

* Improve UX

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-24 22:15:33 +02:00
dependabot[bot] 490bf998a4 chore(deps-dev): bump eslint from 9.26.0 to 9.27.0 in /webui/react-ui (#165)
Bumps [eslint](https://github.com/eslint/eslint) from 9.26.0 to 9.27.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.26.0...v9.27.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.27.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-21 17:33:23 +02:00
dependabot[bot] 9caaaf187a chore(deps-dev): bump @types/react from 19.1.2 to 19.1.4 in /webui/react-ui (#169)
chore(deps-dev): bump @types/react in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-21 17:29:56 +02:00
dependabot[bot] fb1643f1c1 chore(deps-dev): bump @types/react-dom from 19.1.4 to 19.1.5 in /webui/react-ui (#167)
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.4 to 19.1.5.
- [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.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 21:58:32 +02:00
dependabot[bot] 0228499889 chore(deps-dev): bump @eslint/js from 9.26.0 to 9.27.0 in /webui/react-ui (#171)
chore(deps-dev): bump @eslint/js in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.27.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 21:57:52 +02:00
TheGoddessInari 35eb9ee259 ci: enable multi-arch Docker builds for amd64 and arm64 in image workflow (#164) 2025-05-19 08:35:03 +02:00
dependabot[bot] b01d469089 chore(deps-dev): bump @eslint/js from 9.25.1 to 9.26.0 in /webui/react-ui (#138)
chore(deps-dev): bump @eslint/js in /webui/react-ui

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

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.26.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-18 14:21:32 +02:00
dependabot[bot] 843a912a03 chore(deps): bump github.com/valyala/fasthttp from 1.61.0 to 1.62.0 (#150)
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.61.0 to 1.62.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.61.0...v1.62.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-version: 1.62.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-18 14:20:59 +02:00
dependabot[bot] 48f15a479a chore(deps): bump github.com/metoro-io/mcp-golang from 0.11.0 to 0.12.0 (#151)
Bumps [github.com/metoro-io/mcp-golang](https://github.com/metoro-io/mcp-golang) from 0.11.0 to 0.12.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.11.0...v0.12.0)

---
updated-dependencies:
- dependency-name: github.com/metoro-io/mcp-golang
  dependency-version: 0.12.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-18 14:20:38 +02:00
Ettore Di Giacinto 4a0d3a7a94 feat(sshbox): add sshbox to run commands (#161)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-17 23:34:51 +02:00
Ettore Di Giacinto a668830a79 chore(docker-compose): update LocalAI images (#160)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-17 22:17:51 +02:00
AKSizov a3977c2b1c fix: errored ssh output (#158)
fix: explain ssh output
2025-05-16 00:08:50 +02:00
Richard Palethorpe a03098b01e Update README.md 2025-05-15 15:35:54 +01:00
Richard Palethorpe 367832ddb2 feat(core): Add observability for KB lookup
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-05-15 15:35:54 +01:00
Richard Palethorpe 8849a9ba1b fix(matrix): Limit msg history to stop responses to old msgs
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-05-15 15:35:54 +01:00
AKSizov 2b4b2c513c feat: email connector (#157)
* new: add email connection shell

* new: add secure & insecure smtp

* new: read email

* new: more email logic

* feat: automatically reply

* feat: poc email response

* feat: introduce email concurrency and reply functionality

* feat: html replies

* refactor: make email.go legible

* feat: add email connection docs

* fix: startup error handling and dial error
2025-05-15 16:35:39 +02:00
dependabot[bot] e1c44d3f5c chore(deps-dev): bump eslint from 9.25.1 to 9.26.0 in /webui/react-ui
Bumps [eslint](https://github.com/eslint/eslint) from 9.25.1 to 9.26.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.25.1...v9.26.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.26.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-14 09:01:10 +01:00
Richard Palethorpe 112cb1f955 fix(core): Add prompt generated from KB to conv (#156)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-05-14 00:09:43 +02:00
dependabot[bot] a288ea9a36 chore(deps-dev): bump react-router-dom from 7.5.3 to 7.6.0 in /webui/react-ui (#152)
chore(deps-dev): bump react-router-dom in /webui/react-ui

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-13 23:03:53 +02:00
dependabot[bot] 4c2ca24203 chore(deps-dev): bump vite from 6.3.3 to 6.3.5 in /webui/react-ui (#136)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.3 to 6.3.5.
- [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.5/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-13 22:07:43 +02:00
Ettore Di Giacinto f97865f7d8 Update docker-compose.yaml 2025-05-13 22:05:22 +02:00
Ettore Di Giacinto 255435c260 Update docker-compose.yaml 2025-05-13 22:04:20 +02:00
dependabot[bot] fd60daad7a chore(deps-dev): bump @types/react-dom from 19.1.3 to 19.1.4 in /webui/react-ui (#153)
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.3 to 19.1.4.
- [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.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-13 14:39:54 +02:00
Richard Palethorpe 1a53d24890 fix(matrix): Stop Sync Go routine and correct logs (#154)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-05-13 13:10:23 +02: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
217 changed files with 25333 additions and 10048 deletions
+2 -2
View File
@@ -16,11 +16,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: 1.24
- name: Run GoReleaser
+79 -5
View File
@@ -11,10 +11,11 @@ concurrency:
cancel-in-progress: true
jobs:
containerImages:
runs-on: ubuntu-latest
#runs-on: ubuntu-latest
runs-on: arc-runner-localagent
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Prepare
id: prep
@@ -57,7 +58,7 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
with:
images: quay.io/mudler/localagi
tags: |
@@ -78,8 +79,81 @@ jobs:
VERSION=${{ steps.prep.outputs.binary_version }}
context: ./
file: ./Dockerfile.webui
#platforms: linux/amd64,linux/arm64
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
push: true
#tags: ${{ steps.prep.outputs.tags }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
sshbox-build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Prepare
id: prep
run: |
DOCKER_IMAGE=quay.io/mudler/localagi-sshbox
# 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@c299e40c65443455700f0fdfc63efafe5b349051
with:
images: quay.io/mudler/localagi-sshbox
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.sshbox
platforms: linux/amd64,linux/arm64
push: true
#tags: ${{ steps.prep.outputs.tags }}
tags: ${{ steps.meta.outputs.tags }}
+14 -30
View File
@@ -1,4 +1,4 @@
name: Run Go Tests
name: Run Tests
on:
push:
@@ -15,36 +15,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- run: |
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker version
docker run --rm hello-world
- uses: actions/setup-go@v5
uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '>=1.17.0'
go-version: '>=1.26.0'
- name: Run tests
run: |
sudo apt-get update && sudo apt-get install -y make
make tests
#sudo mv coverage/coverage.txt coverage.txt
#sudo chmod 777 coverage.txt
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v4
# with:
# token: ${{ secrets.CODECOV_TOKEN }}
test-e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '>=1.26.0'
- run: |
make tests-e2e
+2
View File
@@ -8,3 +8,5 @@ LocalAGI
**/.env
.vscode
volumes/
example/scheduler/scheduler
example/scheduler/example_tasks.json
+1 -1
View File
@@ -1,5 +1,5 @@
# python
FROM python:3.13-slim
FROM python:3.14-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y python3-dev portaudio19-dev ffmpeg build-essential
+46
View File
@@ -0,0 +1,46 @@
# 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 \
openssh-server \
sudo
# Configure SSH
RUN mkdir /var/run/sshd
RUN echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config
# Create startup script
RUN echo '#!/bin/bash\n\
if [ -n "$SSH_USER" ]; then\n\
if [ "$SSH_USER" = "root" ]; then\n\
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config\n\
if [ -n "$SSH_PASSWORD" ]; then\n\
echo "root:$SSH_PASSWORD" | chpasswd\n\
fi\n\
else\n\
echo "PermitRootLogin no" >> /etc/ssh/sshd_config\n\
useradd -m -s /bin/bash $SSH_USER\n\
if [ -n "$SSH_PASSWORD" ]; then\n\
echo "$SSH_USER:$SSH_PASSWORD" | chpasswd\n\
fi\n\
if [ -n "$SUDO_ACCESS" ] && [ "$SUDO_ACCESS" = "true" ]; then\n\
echo "$SSH_USER ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/$SSH_USER\n\
fi\n\
fi\n\
fi\n\
/usr/sbin/sshd -D' > /start.sh
RUN chmod +x /start.sh
EXPOSE 22
CMD ["/start.sh"]
+15 -6
View File
@@ -16,8 +16,8 @@ COPY webui/react-ui/ ./
# Build the React UI
RUN bun run build
# Use a temporary build image based on Golang 1.24-alpine
FROM golang:1.24-alpine AS builder
# Use a temporary build image based on Golang 1.26-alpine
FROM golang:1.26-alpine AS builder
# Define argument for linker flags
ARG LDFLAGS="-s -w"
@@ -44,12 +44,21 @@ COPY --from=ui-builder /app/dist /work/webui/react-ui/dist
# Build the application
RUN CGO_ENABLED=0 go build -ldflags="$LDFLAGS" -o localagi ./
FROM scratch
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
ca-certificates \
tzdata \
docker.io \
bash \
wget \
curl
# Copy the webui binary from the builder stage to the final image
COPY --from=builder /work/localagi /localagi
COPY --from=builder /etc/ssl/ /etc/ssl/
COPY --from=builder /tmp /tmp
# Define the command that will be run when the container is started
ENTRYPOINT ["/localagi"]
ENTRYPOINT ["/localagi", "serve"]
+5 -2
View File
@@ -9,13 +9,13 @@ cleanup-tests:
docker compose down
tests: prepare-tests
LOCALAGI_MODEL="arcee-agent" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --fail-fast -v -r ./...
LOCALAGI_MODEL="gemma-3-4b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter="!E2E" --flake-attempts=5 --fail-fast -v -r ./...
run-nokb:
$(MAKE) run KBDISABLEINDEX=true
webui/react-ui/dist:
docker run --entrypoint /bin/bash -v $(ROOT_DIR):/app oven/bun:1 -c "cd /app/webui/react-ui && bun install && bun run build"
docker run --entrypoint /bin/bash -v $(ROOT_DIR):/app:z oven/bun:1 -c "cd /app/webui/react-ui && bun install && bun run build"
.PHONY: build
build: webui/react-ui/dist
@@ -30,3 +30,6 @@ build-image:
image-push:
docker push $(IMAGE_NAME)
tests-e2e: prepare-tests
LOCALAGI_MODEL="gemma-3-4b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter="E2E" --flake-attempts=5 --fail-fast -v -r ./tests/e2e/...
+592 -39
View File
@@ -2,7 +2,7 @@
<img src="./webui/react-ui/public/logo_1.png" alt="LocalAGI Logo" width="220"/>
</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">
@@ -11,11 +11,14 @@
[![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)
Try on [![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/LocalAGI_bot)
</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. Create Agents with a couple of clicks, connect via MCP, and use built-in **Skills** (manage skills in the Web UI and enable them per agent). Every agent exposes 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). Skills follow the [skillserver](https://github.com/mudler/skillserver) format and can be created, imported, or synced from git.
## 🛡️ Take Back Your Privacy
@@ -28,15 +31,17 @@ LocalAGI ensures your data stays exactly where you want it—on your hardware. N
- 🎛 **No-Code Agents**: Easy-to-configure multiple agents via Web UI.
- 🖥 **Web-Based Interface**: Simple and intuitive agent management.
- 🤖 **Advanced Agent Teaming**: Instantly create cooperative agent teams from a single prompt.
- 📡 **Connectors Galore**: Built-in integrations with Discord, Slack, Telegram, GitHub Issues, and IRC.
- 📡 **Connectors**: Built-in integrations with Discord, Slack, Telegram, GitHub Issues, and IRC.
- 🛠 **Comprehensive REST API**: Seamless integration into your workflows. Every agent created will support OpenAI Responses API out of the box.
- 📚 **Short & Long-Term Memory**: Powered by [LocalRecall](https://github.com/mudler/LocalRecall).
- 📚 **Short & Long-Term Memory**: Built-in knowledge base (RAG) for collections, file uploads, and semantic search. Manage collections in the Web UI under **Knowledge base**; agents with "Knowledge base" enabled use it automatically (implementation uses [LocalRecall](https://github.com/mudler/LocalRecall) libraries).
- 🧠 **Planning & Reasoning**: Agents intelligently plan, reason, and adapt.
- 🔄 **Periodic Tasks**: Schedule tasks with cron-like syntax.
- 💾 **Memory Management**: Control memory usage with options for long-term and summary memory.
- 🖼 **Multimodal Support**: Ready for vision, text, and more.
- 🔧 **Extensible Custom Actions**: Easily script dynamic agent behaviors in Go (interpreted, no compilation!).
- 📚 **Built-in Skills**: Manage reusable agent skills in the Web UI (create, edit, import/export, git sync). Enable "Skills" per agent to inject skill tools and the skill list into the agent.
- 🛠 **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
@@ -54,12 +59,15 @@ docker compose -f docker-compose.nvidia.yaml up
# Intel GPU setup (for Intel Arc and integrated GPUs)
docker compose -f docker-compose.intel.yaml up
# AMD GPU setup
docker compose -f docker-compose.amd.yaml up
# Start with a specific model (see available models in models.localai.io, or localai.io to use any model in huggingface)
MODEL_NAME=gemma-3-12b-it docker compose up
# NVIDIA GPU setup with custom multimodal and image models
MODEL_NAME=gemma-3-12b-it \
MULTIMODAL_MODEL=minicpm-v-2_6 \
MULTIMODAL_MODEL=moondream2-20250414 \
IMAGE_MODEL=flux.1-dev-ggml \
docker compose -f docker-compose.nvidia.yaml up
```
@@ -68,6 +76,14 @@ 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
## 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://img.youtube.com/vi/d_we-AYksSw/mqdefault.jpg)](https://youtu.be/d_we-AYksSw)
[![RAG and Matrix](https://img.youtube.com/vi/2Xvx78i5oBs/mqdefault.jpg)](https://youtu.be/2Xvx78i5oBs)
## 📚🆕 Local Stack Family
🆕 LocalAI is now part of a comprehensive suite of AI tools designed to work together:
@@ -92,7 +108,7 @@ Still having issues? see this Youtube video: https://youtu.be/HtVwIxW3ePg
</td>
<td width="50%" valign="top">
<h3><a href="https://github.com/mudler/LocalRecall">LocalRecall</a></h3>
<p>A REST-ful API and knowledge base management system that provides persistent memory and storage capabilities for AI agents.</p>
<p>A REST-ful API and knowledge base management system. LocalAGI embeds this functionality: the Web UI includes a <strong>Knowledge base</strong> section and the same collections API, so you no longer need to run LocalRecall separately.</p>
</td>
</tr>
</table>
@@ -114,8 +130,8 @@ LocalAGI supports multiple hardware configurations through Docker Compose profil
- Supports text, multimodal, and image generation models
- Run with: `docker compose -f docker-compose.nvidia.yaml up`
- Default models:
- Text: `arcee-agent`
- Multimodal: `minicpm-v-2_6`
- Text: `gemma-3-4b-it-qat`
- Multimodal: `moondream2-20250414`
- Image: `sd-1.5-ggml`
- Environment variables:
- `MODEL_NAME`: Text model to use
@@ -130,8 +146,8 @@ LocalAGI supports multiple hardware configurations through Docker Compose profil
- Supports text, multimodal, and image generation models
- Run with: `docker compose -f docker-compose.intel.yaml up`
- Default models:
- Text: `arcee-agent`
- Multimodal: `minicpm-v-2_6`
- Text: `gemma-3-4b-it-qat`
- Multimodal: `moondream2-20250414`
- Image: `sd-1.5-ggml`
- Environment variables:
- `MODEL_NAME`: Text model to use
@@ -149,20 +165,23 @@ MODEL_NAME=gemma-3-12b-it docker compose up
# NVIDIA GPU with custom models
MODEL_NAME=gemma-3-12b-it \
MULTIMODAL_MODEL=minicpm-v-2_6 \
MULTIMODAL_MODEL=moondream2-20250414 \
IMAGE_MODEL=flux.1-dev-ggml \
docker compose -f docker-compose.nvidia.yaml up
# Intel GPU with custom models
MODEL_NAME=gemma-3-12b-it \
MULTIMODAL_MODEL=minicpm-v-2_6 \
MULTIMODAL_MODEL=moondream2-20250414 \
IMAGE_MODEL=sd-1.5-ggml \
docker compose -f docker-compose.intel.yaml up
# With custom actions directory
LOCALAGI_CUSTOM_ACTIONS_DIR=/app/custom-actions docker compose up
```
If no models are specified, it will use the defaults:
- Text model: `arcee-agent`
- Multimodal model: `minicpm-v-2_6`
- Text model: `gemma-3-4b-it-qat`
- Multimodal model: `moondream2-20250414`
- Image model: `sd-1.5-ggml`
Good (relatively small) models that have been tested are:
@@ -177,15 +196,7 @@ Good (relatively small) models that have been tested are:
- **✓ Flexible Model Integration**: Supports GGUF, GGML, and more thanks to [LocalAI](https://github.com/mudler/LocalAI).
- **✓ Developer-Friendly**: Rich APIs and intuitive interfaces.
- **✓ 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.
## 🌐 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.
- **✓ Feature-Rich**: From planning to multimodal capabilities, connectors for Slack, MCP support, built-in Skills, LocalAGI has it all.
## 🌟 Screenshots
@@ -194,6 +205,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 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 Agent Observability](https://github.com/user-attachments/assets/f7359048-9d28-4cf1-9151-1f5556ce9235)
### Connectors Ready-to-Go
@@ -212,6 +225,7 @@ Explore detailed documentation including:
- [REST API Documentation](#rest-api)
- [Connector Configuration](#connectors)
- [Agent Configuration](#agent-configuration-reference)
- [Skills](#3-skills)
### Environment Configuration
@@ -225,9 +239,15 @@ LocalAGI supports environment configurations. Note that these environment variab
| `LOCALAGI_LLM_API_KEY` | API authentication |
| `LOCALAGI_TIMEOUT` | Request timeout settings |
| `LOCALAGI_STATE_DIR` | Where state gets stored |
| `LOCALAGI_LOCALRAG_URL` | LocalRecall connection |
| `LOCALAGI_LOCALRAG_URL` | Optional URL when using an external LocalRAG URL; not used for built-in knowledge base |
| `LOCALAGI_BASE_URL` | Optional base URL for the app (defaults to ":3000") |
| `LOCALAGI_ENABLE_CONVERSATIONS_LOGGING` | Toggle conversation logs |
| `LOCALAGI_API_KEYS` | A comma separated list of api keys used for authentication |
| `LOCALAGI_CUSTOM_ACTIONS_DIR` | Directory containing custom Go action files to be automatically loaded |
For the built-in knowledge base, optional env (defaults use `LOCALAGI_STATE_DIR`): `COLLECTION_DB_PATH`, `FILE_ASSETS`, `VECTOR_ENGINE` (e.g. `chromem`, `postgres`), `EMBEDDING_MODEL`, `DATABASE_URL` (when `VECTOR_ENGINE=postgres`).
Skills are stored in a fixed `skills` subdirectory under `LOCALAGI_STATE_DIR` (e.g. `/pool/skills` in Docker). Git repo config for skills lives in that directory. No extra environment variables are required.
## Installation Options
@@ -256,6 +276,442 @@ go build -o 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 (call pool.SetRAGProvider(...) for knowledge base; see main.go)
pool, err := state.NewAgentPool(
"default-model", // default model name
"default-multimodal-model", // default multimodal model
"transcription-model", // default transcription model
"en", // default transcription language
"tts-model", // default TTS model
"http://localhost:8080", // API URL
"your-api-key", // API key
"./state", // state directory
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
nil, // skills service (optional)
)
// 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>
## 🔧 Extending LocalAGI
LocalAGI provides two powerful ways to extend its functionality with custom actions:
### 1. Custom Actions (Go Code)
LocalAGI supports custom actions written in Go that can be defined inline when creating an agent. These actions are interpreted at runtime, so no compilation is required.
#### Automatic Custom Actions Loading
You can also place custom Go action files in a directory and have LocalAGI automatically load them. Set the `LOCALAGI_CUSTOM_ACTIONS_DIR` environment variable to point to a directory containing your custom action files. Each `.go` file in this directory will be automatically loaded and made available to all agents.
**Example setup:**
```bash
# Set the environment variable
export LOCALAGI_CUSTOM_ACTIONS_DIR="/path/to/custom/actions"
# Or in docker-compose.yaml
environment:
- LOCALAGI_CUSTOM_ACTIONS_DIR=/app/custom-actions
```
**Directory structure:**
```
custom-actions/
├── weather_action.go
├── file_processor.go
└── database_query.go
```
Each file should contain the three required functions (`Run`, `Definition`, `RequiredFields`) as described below.
#### How Custom Actions Work
When creating a new Agent, in the action sections select the "custom" action, you can add the Golang code directly there.
Custom actions in LocalAGI require three main functions:
1. **`Run(config map[string]interface{}) (string, map[string]interface{}, error)`** - The main execution function
2. **`Definition() map[string][]string`** - Defines the action's parameters and their types
3. **`RequiredFields() []string`** - Specifies which parameters are required
Note: You can't use additional modules, but just use libraries that are included in Go.
#### Example: Weather Information Action
Here's a practical example of a custom action that fetches weather information:
```go
import (
"encoding/json"
"fmt"
"net/http"
"io"
)
type WeatherParams struct {
City string `json:"city"`
Country string `json:"country"`
}
type WeatherResponse struct {
Main struct {
Temp float64 `json:"temp"`
Humidity int `json:"humidity"`
} `json:"main"`
Weather []struct {
Description string `json:"description"`
} `json:"weather"`
}
func Run(config map[string]interface{}) (string, map[string]interface{}, error) {
// Parse parameters
p := WeatherParams{}
b, err := json.Marshal(config)
if err != nil {
return "", map[string]interface{}{}, err
}
if err := json.Unmarshal(b, &p); err != nil {
return "", map[string]interface{}{}, err
}
// Make API call to weather service
url := fmt.Sprintf("http://api.openweathermap.org/data/2.5/weather?q=%s,%s&appid=YOUR_API_KEY&units=metric", p.City, p.Country)
resp, err := http.Get(url)
if err != nil {
return "", map[string]interface{}{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", map[string]interface{}{}, err
}
var weather WeatherResponse
if err := json.Unmarshal(body, &weather); err != nil {
return "", map[string]interface{}{}, err
}
// Format response
result := fmt.Sprintf("Weather in %s, %s: %.1f°C, %s, Humidity: %d%%",
p.City, p.Country, weather.Main.Temp, weather.Weather[0].Description, weather.Main.Humidity)
return result, map[string]interface{}{}, nil
}
func Definition() map[string][]string {
return map[string][]string{
"city": []string{
"string",
"The city name to get weather for",
},
"country": []string{
"string",
"The country code (e.g., US, UK, DE)",
},
}
}
func RequiredFields() []string {
return []string{"city", "country"}
}
```
#### Example: File System Action
Here's another example that demonstrates file system operations:
```go
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
type FileParams struct {
Path string `json:"path"`
Action string `json:"action"`
Content string `json:"content,omitempty"`
}
func Run(config map[string]interface{}) (string, map[string]interface{}, error) {
p := FileParams{}
b, err := json.Marshal(config)
if err != nil {
return "", map[string]interface{}{}, err
}
if err := json.Unmarshal(b, &p); err != nil {
return "", map[string]interface{}{}, err
}
switch p.Action {
case "read":
content, err := os.ReadFile(p.Path)
if err != nil {
return "", map[string]interface{}{}, err
}
return string(content), map[string]interface{}{}, nil
case "write":
err := os.WriteFile(p.Path, []byte(p.Content), 0644)
if err != nil {
return "", map[string]interface{}{}, err
}
return fmt.Sprintf("Successfully wrote to %s", p.Path), map[string]interface{}{}, nil
case "list":
files, err := os.ReadDir(p.Path)
if err != nil {
return "", map[string]interface{}{}, err
}
var fileList []string
for _, file := range files {
fileList = append(fileList, file.Name())
}
result, _ := json.Marshal(fileList)
return string(result), map[string]interface{}{}, nil
default:
return "", map[string]interface{}{}, fmt.Errorf("unknown action: %s", p.Action)
}
}
func Definition() map[string][]string {
return map[string][]string{
"path": []string{
"string",
"The file or directory path",
},
"action": []string{
"string",
"The action to perform: read, write, or list",
},
"content": []string{
"string",
"Content to write (required for write action)",
},
}
}
func RequiredFields() []string {
return []string{"path", "action"}
}
```
#### Using Custom Actions in Agents
To use custom actions, add them to your agent configuration:
1. **Via Web UI**: In the agent creation form, add a "Custom" action and paste your Go code
2. **Via API**: Include the custom action in your agent configuration JSON
3. **Via Library**: Add the custom action to your agent's actions list
### 2. MCP (Model Context Protocol) Servers
LocalAGI supports both local and remote MCP servers, allowing you to extend functionality with external tools and services.
#### What is MCP?
The Model Context Protocol (MCP) is a standard for connecting AI applications to external data sources and tools. LocalAGI can connect to any MCP-compliant server to access additional capabilities.
#### Local MCP Servers
Local MCP servers run as processes that LocalAGI can spawn and communicate with via STDIO.
##### Example: GitHub MCP Server
```json
{
"mcpServers": {
"github": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
}
}
}
}
```
#### Remote MCP Servers
Remote MCP servers are HTTP-based and can be accessed over the network.
#### Creating Your Own MCP Server
You can create MCP servers in any language that supports the MCP protocol and add the URLs of the servers to LocalAGI.
#### Configuring MCP Servers in LocalAGI
1. **Via Web UI**: In the MCP Settings section of agent creation, add MCP servers
2. **Via API**: Include MCP server configuration in your agent config
#### Best Practices
- **Security**: Always validate inputs and use proper authentication for remote MCP servers
- **Error Handling**: Implement robust error handling in your MCP servers
- **Documentation**: Provide clear descriptions for all tools exposed by your MCP server
- **Testing**: Test your MCP servers independently before integrating with LocalAGI
- **Resource Management**: Ensure your MCP servers properly clean up resources
### 3. Skills
LocalAGI includes built-in **Skills** management. Skills are reusable instructions and resources (scripts, references, assets) that agents can use when "Enable Skills" is turned on for that agent.
- **Skills section (Web UI)**: Open **Skills** in the sidebar. Skills are stored under the state directory (`STATE_DIR/skills`). Create, edit, search, import, and export skills. You can also add git repositories to sync skills from.
- **Per-agent**: In agent creation or settings, enable **Enable Skills** in Advanced Settings. The agent will receive a list of available skills in its context and have access to skill tools (list, read, search, resources) via the built-in skills MCP.
- Skills use the same format as [skillserver](https://github.com/mudler/skillserver) (e.g. `SKILL.md` in a directory). You can export skills from LocalAGI and use them with the standalone skillserver, or import skills created elsewhere.
In Docker, the state directory is persisted (`/pool`), so skills are stored in `/pool/skills`. To use a host folder for skills, mount it over that path in your compose file (e.g. `- ./my-skills:/pool/skills`).
### Development
The development workflow is similar to the source build, but with additional steps for hot reloading of the frontend:
@@ -265,15 +721,39 @@ The development workflow is similar to the source build, but with additional ste
git clone https://github.com/mudler/LocalAGI.git
cd LocalAGI
# Install dependencies and start frontend development server
cd webui/react-ui && bun i && bun run dev
cd webui/react-ui
# Install dependencies
bun i
# Compile frontend (the build directory needs to exist for the backend to start)
bun run build
# Start frontend development server
bun run dev
```
Then in separate terminal:
```bash
cd LocalAGI
# Create a "pool" directory for agent state
mkdir pool
# Set required environment variables
export LOCALAGI_MODEL=gemma-3-4b-it-qat
export LOCALAGI_MULTIMODAL_MODEL=moondream2-20250414
export LOCALAGI_IMAGE_MODEL=sd-1.5-ggml
export LOCALAGI_LLM_API_URL=http://localai:8080
# Knowledge base is built-in; no separate LocalRecall service needed
export LOCALAGI_STATE_DIR=./pool
export LOCALAGI_TIMEOUT=5m
export LOCALAGI_ENABLE_CONVERSATIONS_LOGGING=false
export LOCALAGI_SSHBOX_URL=root:root@sshbox:22
# Start development server
cd ../.. && go run main.go
go run main.go
```
> Note: see webui/react-ui/.vite.config.js for env vars that can be used to configure the backend URL
@@ -282,7 +762,8 @@ cd ../.. && go run main.go
Link your agents to the services you already use. Configuration examples below.
### GitHub Issues
<details>
<summary><strong>GitHub Issues</strong></summary>
```json
{
@@ -292,8 +773,10 @@ Link your agents to the services you already use. Configuration examples below.
"botUserName": "bot-username"
}
```
</details>
### Discord
<details>
<summary><strong>Discord</strong></summary>
After [creating your Discord bot](https://discordpy.readthedocs.io/en/stable/discord.html):
@@ -305,8 +788,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!
> 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:
@@ -319,19 +804,39 @@ 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 App level token (from "Basic Information" -> "App-Level Tokens" ( scope connections:writeRoute authorizations:read ))
</details>
### Telegram
<details>
<summary><strong>Telegram</strong></summary>
Get a token from @botfather, then:
```json
{
"token": "your-bot-father-token"
"token": "your-bot-father-token",
"group_mode": "true",
"mention_only": "true",
"admins": "username1,username2"
}
```
### IRC
Configuration options:
- `token`: Your bot token from BotFather
- `group_mode`: Enable/disable group chat functionality
- `mention_only`: When enabled, bot only responds when mentioned in groups
- `admins`: Comma-separated list of Telegram usernames allowed to use the bot in private chats
- `channel_id`: Optional channel ID for the bot to send messages to
> **Important**: For group functionality to work properly:
> 1. Go to @BotFather
> 2. Select your bot
> 3. Go to "Bot Settings" > "Group Privacy"
> 4. Select "Turn off" to allow the bot to read all messages in groups
> 5. Restart your bot after changing this setting
</details>
<details>
<summary><strong>IRC</strong></summary>
Connect to IRC networks:
@@ -344,10 +849,29 @@ Connect to IRC networks:
"alwaysReply": "false"
}
```
</details>
<details>
<summary><strong>Email</strong></summary>
```json
{
"smtpServer": "smtp.gmail.com:587",
"imapServer": "imap.gmail.com:993",
"smtpInsecure": "false",
"imapInsecure": "false",
"username": "user@gmail.com",
"email": "user@gmail.com",
"password": "correct-horse-battery-staple",
"name": "LogalAGI Agent"
}
```
</details>
## REST API
### Agent Management
<details>
<summary><strong>Agent Management</strong></summary>
| Endpoint | Method | Description | Example |
|----------|--------|-------------|---------|
@@ -362,8 +886,10 @@ Connect to IRC networks:
| `/api/meta/agent/config` | GET | Get agent configuration metadata | |
| `/settings/export/:name` | GET | Export agent config | [Example](#export-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 |
|----------|--------|-------------|---------|
@@ -371,8 +897,10 @@ Connect to IRC networks:
| `/api/action/:name/run` | POST | Execute an action | |
| `/api/agent/group/generateProfiles` | POST | Generate group profiles | |
| `/api/agent/group/create` | POST | Create a new agent group | |
</details>
### Chat Interactions
<details>
<summary><strong>Chat Interactions</strong></summary>
| Endpoint | Method | Description | Example |
|----------|--------|-------------|---------|
@@ -380,6 +908,7 @@ Connect to IRC networks:
| `/api/notify/:name` | POST | Send notification to agent | [Example](#notify-agent) |
| `/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) |
</details>
<details>
<summary><strong>Curl Examples</strong></summary>
@@ -467,11 +996,13 @@ curl -X POST "http://localhost:3000/api/notify/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.
</details>
### 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:
```bash
@@ -504,6 +1035,28 @@ Here's an example of the agent configuration structure:
"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` | Optional URL when using an external LocalRAG URL; not used for built-in knowledge base |
| `LOCALAGI_BASE_URL` | Optional base URL for the app (defaults to ":3000") |
| `LOCALAGI_SSHBOX_URL` | LocalAGI SSHBox URL, e.g. user:pass@ip:port |
| `LOCALAGI_ENABLE_CONVERSATIONS_LOGGING` | Toggle conversation logs |
| `LOCALAGI_API_KEYS` | A comma separated list of api keys used for authentication |
| `LOCALAGI_CUSTOM_ACTIONS_DIR` | Directory containing custom Go action files to be automatically loaded |
</details>
## LICENSE
+15
View File
@@ -0,0 +1,15 @@
package cmd
import (
"github.com/spf13/cobra"
)
var agentCmd = &cobra.Command{
Use: "agent",
Short: "Manage agents",
Long: "Commands for managing and running LocalAGI agents.",
}
func init() {
agentCmd.AddCommand(agentRunCmd)
}
+355
View File
@@ -0,0 +1,355 @@
package cmd
import (
"encoding/json"
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"github.com/mudler/LocalAGI/core/state"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/services"
"github.com/mudler/LocalAGI/services/skills"
"github.com/spf13/cobra"
)
var (
configFile string
prompt string
)
var agentRunCmd = &cobra.Command{
Use: "run [agent_name]",
Short: "Run an agent standalone",
Long: `Run an agent without starting the web server.
Two modes are supported:
1. Run an agent by name from the registry (pool.json):
local-agi agent run my-agent
2. Run an agent from a JSON config file:
local-agi agent run --config agent.json
3. Run an agent in foreground mode with a prompt:
local-agi agent run my-agent --prompt "Your question here"
The agent runs in the foreground until interrupted (Ctrl+C).`,
Args: cobra.MaximumNArgs(1),
RunE: runAgent,
}
func init() {
agentRunCmd.Flags().StringVarP(&configFile, "config", "c", "", "path to agent JSON config file")
agentRunCmd.Flags().StringVarP(&prompt, "prompt", "p", "", "run in foreground mode with the given prompt and exit after response")
}
func runAgent(cmd *cobra.Command, args []string) error {
agentName, agentConfig, err := resolveAgentConfig(args)
if err != nil {
return err
}
// If --prompt is provided, run in foreground mode
if prompt != "" {
return runAgentForeground(agentName, agentConfig, prompt)
}
return startStandaloneAgent(agentName, agentConfig)
}
// runAgentForeground runs an agent in foreground mode with a single prompt,
// prints the response, and exits.
func runAgentForeground(agentName string, agentConfig *state.AgentConfig, promptText string) error {
// Load all environment variables
env := LoadEnv()
if env.Model == "" {
env.Model = agentConfig.Model
}
if env.LLMAPIURL == "" {
env.LLMAPIURL = agentConfig.APIURL
}
if env.LLMAPIKey == "" {
env.LLMAPIKey = agentConfig.APIKey
}
if env.Model == "" {
return fmt.Errorf("model not set: provide 'model' in config or set LOCALAGI_MODEL")
}
if env.LLMAPIURL == "" {
return fmt.Errorf("API URL not set: provide 'api_url' in config or set LOCALAGI_LLM_API_URL")
}
if env.StateDir == "" {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
}
env.StateDir = filepath.Join(cwd, "pool")
}
os.MkdirAll(env.StateDir, 0755)
// Override config with resolved values
agentConfig.Model = env.Model
agentConfig.APIURL = env.LLMAPIURL
agentConfig.APIKey = env.LLMAPIKey
agentConfig.MultimodalModel = env.MultimodalModel
agentConfig.TranscriptionModel = env.TranscriptionModel
agentConfig.TranscriptionLanguage = env.TranscriptionLanguage
agentConfig.TTSModel = env.TTSModel
// Initialize skills service
skillsService, err := skills.NewService(env.StateDir)
if err != nil {
return fmt.Errorf("failed to initialize skills service: %w", err)
}
// Build service factories
actionsFactory := services.Actions(map[string]string{
services.ActionConfigSSHBoxURL: env.SSHBoxURL,
services.ConfigStateDir: env.StateDir,
services.CustomActionsDir: env.CustomActionsDir,
})
dynamicPromptsFactory := services.DynamicPrompts(map[string]string{
services.ConfigStateDir: env.StateDir,
services.CustomActionsDir: env.CustomActionsDir,
})
// Create the pool
pool, err := state.NewAgentPool(
env.Model, env.MultimodalModel, env.TranscriptionModel, env.TranscriptionLanguage, env.TTSModel,
env.LLMAPIURL, env.LLMAPIKey, env.StateDir,
actionsFactory, services.Connectors, dynamicPromptsFactory, services.Filters,
env.Timeout, false, skillsService,
)
if err != nil {
return fmt.Errorf("failed to create agent pool: %w", err)
}
if env.LocalRAGURL != "" {
pool.SetRAGProvider(state.NewHTTPRAGProvider(env.LocalRAGURL, env.LLMAPIKey))
}
// Start the agent
if err := pool.StartAgentStandalone(agentName, agentConfig); err != nil {
return fmt.Errorf("failed to start agent: %w", err)
}
a := pool.GetAgent(agentName)
if a == nil {
return fmt.Errorf("agent %q was not found after starting", agentName)
}
fmt.Fprintf(os.Stderr, "Running agent %q in foreground mode with prompt...\n", agentName)
// Execute Ask with the prompt using WithText option
result := a.Ask(types.WithText(promptText))
// Print the result
if result.Error != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", result.Error.Error())
pool.Stop(agentName)
return fmt.Errorf("agent error: %s", result.Error.Error())
}
// Print the response
fmt.Println(result.Response)
// Clean up
pool.Stop(agentName)
return nil
}
// resolveAgentConfig determines the agent name and config from either
// a registry lookup or a JSON config file.
func resolveAgentConfig(args []string) (string, *state.AgentConfig, error) {
if configFile != "" && len(args) > 0 {
return "", nil, fmt.Errorf("cannot specify both --config and agent name; use one or the other")
}
if configFile == "" && len(args) == 0 {
return "", nil, fmt.Errorf("either an agent name or --config <file> is required")
}
if configFile != "" {
return loadConfigFromFile(configFile)
}
return loadConfigFromRegistry(args[0])
}
// loadConfigFromFile reads and validates an agent config from a JSON file.
func loadConfigFromFile(path string) (string, *state.AgentConfig, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", nil, fmt.Errorf("failed to read config file %q: %w", path, err)
}
var config state.AgentConfig
if err := json.Unmarshal(data, &config); err != nil {
return "", nil, fmt.Errorf("failed to parse config file %q: %w", path, err)
}
if err := validateConfig(&config); err != nil {
return "", nil, fmt.Errorf("invalid config in %q: %w", path, err)
}
name := config.Name
if name == "" {
// Derive name from filename
base := filepath.Base(path)
name = base[:len(base)-len(filepath.Ext(base))]
config.Name = name
}
return name, &config, nil
}
// loadConfigFromRegistry loads an agent config from the pool.json registry.
func loadConfigFromRegistry(name string) (string, *state.AgentConfig, error) {
stateDir := os.Getenv("LOCALAGI_STATE_DIR")
if stateDir == "" {
cwd, err := os.Getwd()
if err != nil {
return "", nil, fmt.Errorf("failed to get working directory: %w", err)
}
stateDir = filepath.Join(cwd, "pool")
}
poolFile := filepath.Join(stateDir, "pool.json")
data, err := os.ReadFile(poolFile)
if err != nil {
return "", nil, fmt.Errorf("failed to read pool file %q: %w\nEnsure LOCALAGI_STATE_DIR is set or a pool/ directory exists", poolFile, err)
}
var pool map[string]state.AgentConfig
if err := json.Unmarshal(data, &pool); err != nil {
return "", nil, fmt.Errorf("failed to parse pool file %q: %w", poolFile, err)
}
config, exists := pool[name]
if !exists {
available := make([]string, 0, len(pool))
for k := range pool {
available = append(available, k)
}
return "", nil, fmt.Errorf("agent %q not found in registry\nAvailable agents: %v", name, available)
}
return name, &config, nil
}
// validateConfig checks that required fields are present in the config.
func validateConfig(config *state.AgentConfig) error {
// Model and API URL can come from env vars, so they're not strictly required in config.
// But we validate that the config is at least parseable (already done by JSON unmarshal).
return nil
}
// startStandaloneAgent creates and runs a single agent using the pool,
// without starting the web server.
func startStandaloneAgent(name string, config *state.AgentConfig) error {
// Load all environment variables
env := LoadEnv()
if env.Model == "" {
env.Model = config.Model
}
if env.LLMAPIURL == "" {
env.LLMAPIURL = config.APIURL
}
if env.LLMAPIKey == "" {
env.LLMAPIKey = config.APIKey
}
if env.Model == "" {
return fmt.Errorf("model not set: provide 'model' in config or set LOCALAGI_MODEL")
}
if env.LLMAPIURL == "" {
return fmt.Errorf("API URL not set: provide 'api_url' in config or set LOCALAGI_LLM_API_URL")
}
if env.StateDir == "" {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
}
env.StateDir = filepath.Join(cwd, "pool")
}
os.MkdirAll(env.StateDir, 0755)
// Override config with resolved values
config.Model = env.Model
config.APIURL = env.LLMAPIURL
config.APIKey = env.LLMAPIKey
config.MultimodalModel = env.MultimodalModel
config.TranscriptionModel = env.TranscriptionModel
config.TranscriptionLanguage = env.TranscriptionLanguage
config.TTSModel = env.TTSModel
if config.PeriodicRuns == "" {
config.PeriodicRuns = "10m"
}
if config.SchedulerPollInterval == "" {
config.SchedulerPollInterval = "30s"
}
// Initialize skills service
skillsService, err := skills.NewService(env.StateDir)
if err != nil {
return fmt.Errorf("failed to initialize skills service: %w", err)
}
// Build service factories
actionsFactory := services.Actions(map[string]string{
services.ActionConfigSSHBoxURL: env.SSHBoxURL,
services.ConfigStateDir: env.StateDir,
services.CustomActionsDir: env.CustomActionsDir,
})
dynamicPromptsFactory := services.DynamicPrompts(map[string]string{
services.ConfigStateDir: env.StateDir,
services.CustomActionsDir: env.CustomActionsDir,
})
// Create the pool and use it to start the agent
pool, err := state.NewAgentPool(
env.Model, env.MultimodalModel, env.TranscriptionModel, env.TranscriptionLanguage, env.TTSModel,
env.LLMAPIURL, env.LLMAPIKey, env.StateDir,
actionsFactory, services.Connectors, dynamicPromptsFactory, services.Filters,
env.Timeout, false, skillsService,
)
if err != nil {
return fmt.Errorf("failed to create agent pool: %w", err)
}
if env.LocalRAGURL != "" {
pool.SetRAGProvider(state.NewHTTPRAGProvider(env.LocalRAGURL, env.LLMAPIKey))
}
// Start the agent via the pool (handles all option building, connectors, etc.)
if err := pool.StartAgentStandalone(name, config); err != nil {
return fmt.Errorf("failed to start agent: %w", err)
}
a := pool.GetAgent(name)
if a == nil {
return fmt.Errorf("agent %q was not found after starting", name)
}
fmt.Fprintf(os.Stderr, "Starting agent %q (model: %s, api: %s)\n", name, env.Model, env.LLMAPIURL)
fmt.Fprintf(os.Stderr, "Press Ctrl+C to stop\n")
// Wait for interrupt
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigCh
fmt.Fprintf(os.Stderr, "\nReceived %v, stopping agent...\n", sig)
pool.Stop(name)
// Give agent a moment to clean up
time.Sleep(2 * time.Second)
return nil
}
+106
View File
@@ -0,0 +1,106 @@
package cmd
import (
"os"
"strconv"
"strings"
)
// Env contains all environment variables used by LocalAGI
type Env struct {
// Model and API configuration
Model string
LLMAPIURL string
LLMAPIKey string
MultimodalModel string
TranscriptionModel string
TranscriptionLanguage string
TTSModel string
Timeout string
// Directories and paths
StateDir string
LocalAGIURL string
LocalRAGURL string
CustomActionsDir string
SSHBoxURL string
CollectionDBPath string
FileAssets string
// Conversation settings
EnableConversationsLogging bool
APIKeys []string
ConversationDuration string
// RAG/Vector settings
VectorEngine string
EmbeddingModel string
MaxChunkingSize int
ChunkOverlap int
DatabaseURL string
}
// LoadEnv reads all environment variables and returns an Env struct
func LoadEnv() Env {
env := Env{
Model: envOrDefault("LOCALAGI_MODEL", ""),
LLMAPIURL: envOrDefault("LOCALAGI_LLM_API_URL", ""),
LLMAPIKey: envOrDefault("LOCALAGI_LLM_API_KEY", ""),
MultimodalModel: envOrDefault("LOCALAGI_MULTIMODAL_MODEL", ""),
TranscriptionModel: envOrDefault("LOCALAGI_TRANSCRIPTION_MODEL", ""),
TranscriptionLanguage: envOrDefault("LOCALAGI_TRANSCRIPTION_LANGUAGE", ""),
TTSModel: envOrDefault("LOCALAGI_TTS_MODEL", ""),
Timeout: envOrDefault("LOCALAGI_TIMEOUT", "5m"),
StateDir: envOrDefault("LOCALAGI_STATE_DIR", ""),
LocalAGIURL: envOrDefault("LOCALAGI_BASE_URL", ":3000"),
LocalRAGURL: os.Getenv("LOCALAGI_LOCALRAG_URL"),
CustomActionsDir: os.Getenv("LOCALAGI_CUSTOM_ACTIONS_DIR"),
SSHBoxURL: os.Getenv("LOCALAGI_SSHBOX_URL"),
EnableConversationsLogging: os.Getenv("LOCALAGI_ENABLE_CONVERSATIONS_LOGGING") == "true",
ConversationDuration: os.Getenv("LOCALAGI_CONVERSATION_DURATION"),
CollectionDBPath: os.Getenv("COLLECTION_DB_PATH"),
FileAssets: os.Getenv("FILE_ASSETS"),
VectorEngine: os.Getenv("VECTOR_ENGINE"),
EmbeddingModel: os.Getenv("EMBEDDING_MODEL"),
DatabaseURL: os.Getenv("DATABASE_URL"),
}
// Parse APIKeys from comma-separated string
if apiKeysEnv := os.Getenv("LOCALAGI_API_KEYS"); apiKeysEnv != "" {
env.APIKeys = strings.Split(apiKeysEnv, ",")
}
// Parse numeric values
if maxChunkingSizeEnv := os.Getenv("MAX_CHUNKING_SIZE"); maxChunkingSizeEnv != "" {
if n, err := strconv.Atoi(maxChunkingSizeEnv); err == nil {
env.MaxChunkingSize = n
}
}
if chunkOverlapEnv := os.Getenv("CHUNK_OVERLAP"); chunkOverlapEnv != "" {
if n, err := strconv.Atoi(chunkOverlapEnv); err == nil {
env.ChunkOverlap = n
}
}
// Set defaults for empty values
if env.VectorEngine == "" {
env.VectorEngine = "chromem"
}
if env.EmbeddingModel == "" {
env.EmbeddingModel = "granite-embedding-107m-multilingual"
}
if env.MaxChunkingSize == 0 {
env.MaxChunkingSize = 400
}
return env
}
// envOrDefault returns the environment variable value if set, otherwise the fallback.
func envOrDefault(envKey, fallback string) string {
if v := os.Getenv(envKey); v != "" {
return v
}
return fallback
}
+32
View File
@@ -0,0 +1,32 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "local-agi",
Short: "LocalAGI - Self-hosted AI Agent platform",
Long: "LocalAGI is a self-hosted AI Agent platform that allows running autonomous agents with various connectors, actions, and tools.",
RunE: func(cmd *cobra.Command, args []string) error {
// If no subcommand is provided, default to serving the web server
// This ensures the container starts the web server by default
return serveCmd.RunE(cmd, args)
},
}
// Execute runs the root command.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
rootCmd.AddCommand(serveCmd)
rootCmd.AddCommand(agentCmd)
}
+128
View File
@@ -0,0 +1,128 @@
package cmd
import (
"log"
"os"
"path/filepath"
"github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/core/state"
"github.com/mudler/LocalAGI/services"
"github.com/mudler/LocalAGI/services/skills"
"github.com/mudler/LocalAGI/webui"
"github.com/spf13/cobra"
)
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the LocalAGI web server",
Long: "Start the LocalAGI web server with the agent pool and web UI.",
RunE: runServe,
}
func init() {
rootCmd.AddCommand(serveCmd)
}
func runServe(cmd *cobra.Command, args []string) error {
// Load all environment variables
env := LoadEnv()
if env.Model == "" {
return cmd.Help()
}
if env.LLMAPIURL == "" {
return cmd.Help()
}
if env.StateDir == "" {
cwd, err := os.Getwd()
if err != nil {
return err
}
env.StateDir = filepath.Join(cwd, "pool")
}
os.MkdirAll(env.StateDir, 0755)
if env.CollectionDBPath == "" {
env.CollectionDBPath = filepath.Join(env.StateDir, "collections")
}
if env.FileAssets == "" {
env.FileAssets = filepath.Join(env.StateDir, "assets")
}
apiKeys := env.APIKeys
if len(apiKeys) == 0 {
apiKeys = []string{}
}
skillsService, err := skills.NewService(env.StateDir)
if err != nil {
return err
}
pool, err := state.NewAgentPool(
env.Model,
env.MultimodalModel,
env.TranscriptionModel,
env.TranscriptionLanguage,
env.TTSModel,
env.LLMAPIURL,
env.LLMAPIKey,
env.StateDir,
services.Actions(map[string]string{
services.ActionConfigSSHBoxURL: env.SSHBoxURL,
services.ConfigStateDir: env.StateDir,
services.CustomActionsDir: env.CustomActionsDir,
}),
services.Connectors,
services.DynamicPrompts(map[string]string{
services.ConfigStateDir: env.StateDir,
services.CustomActionsDir: env.CustomActionsDir,
}),
services.Filters,
env.Timeout,
env.EnableConversationsLogging,
skillsService,
)
if err != nil {
return err
}
app := webui.NewApp(
webui.WithPool(pool),
webui.WithSkillsService(skillsService),
webui.WithConversationStoreduration(env.ConversationDuration),
webui.WithApiKeys(apiKeys...),
webui.WithLLMAPIUrl(env.LLMAPIURL),
webui.WithLLMAPIKey(env.LLMAPIKey),
webui.WithLLMModel(env.Model),
webui.WithCustomActionsDir(env.CustomActionsDir),
webui.WithStateDir(env.StateDir),
webui.WithCollectionDBPath(env.CollectionDBPath),
webui.WithFileAssets(env.FileAssets),
webui.WithVectorEngine(env.VectorEngine),
webui.WithEmbeddingModel(env.EmbeddingModel),
webui.WithMaxChunkingSize(env.MaxChunkingSize),
webui.WithChunkOverlap(env.ChunkOverlap),
webui.WithDatabaseURL(env.DatabaseURL),
webui.WithLocalRAGURL(env.LocalRAGURL),
)
if env.LocalRAGURL != "" {
pool.SetRAGProvider(state.NewHTTPRAGProvider(env.LocalRAGURL, env.LLMAPIKey))
} else {
embedded := app.CollectionsRAGProvider()
pool.SetRAGProvider(func(collectionName, _, _ string) (agent.RAGDB, state.KBCompactionClient, bool) {
return embedded(collectionName)
})
}
if err := pool.StartAll(); err != nil {
return err
}
log.Fatal(app.Listen(env.LocalAGIURL))
return nil
}
+49 -7
View File
@@ -3,11 +3,13 @@ package action
import (
"context"
"fmt"
"os"
"regexp"
"strings"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/xlog"
"github.com/sashabaranov/go-openai/jsonschema"
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
@@ -24,7 +26,7 @@ func NewCustom(config map[string]string, goPkgPath string) (*CustomAction, error
}
if err := a.callInit(); err != nil {
xlog.Error("Error calling custom action init", "error", err)
xlog.Warn("No init function found for custom action", "error", err, "action", a.config["name"])
}
return a, nil
@@ -43,18 +45,23 @@ func (a *CustomAction) callInit() error {
v, err := a.i.Eval(fmt.Sprintf("%s.Init", a.config["name"]))
if err != nil {
return err
xlog.Warn("No init function found for custom action", "error", err, "action", a.config["name"])
return nil
}
run := v.Interface().(func() error)
run, ok := v.Interface().(func(string) error)
if !ok {
return nil
}
return run()
return run(a.config["configuration"])
}
func (a *CustomAction) initializeInterpreter() error {
if _, exists := a.config["code"]; exists && a.i == nil {
unsafe := strings.ToLower(a.config["unsafe"]) == "true"
i := interp.New(interp.Options{
Env: os.Environ(),
GoPath: a.goPkgPath,
Unrestricted: unsafe,
})
@@ -66,6 +73,15 @@ func (a *CustomAction) initializeInterpreter() error {
a.config["name"] = "custom"
}
// let's find first if there is already a package declarated in the code
// the user might want to specify it to not break syntax with IDEs
re := regexp.MustCompile("package (\\w+)")
packageName := re.FindStringSubmatch(a.config["code"])
if len(packageName) > 1 {
// remove it from the code, normalize to `name`
a.config["code"] = re.ReplaceAllString(a.config["code"], "")
}
_, err := i.Eval(fmt.Sprintf("package %s\n%s", a.config["name"], a.config["code"]))
if err != nil {
return err
@@ -81,7 +97,7 @@ func (a *CustomAction) Plannable() bool {
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"]))
if err != nil {
return types.ActionResult{}, err
@@ -95,12 +111,32 @@ func (a *CustomAction) Run(ctx context.Context, params types.ActionParams) (type
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"]))
if err != nil {
xlog.Error("Error getting custom action definition", "error", err)
return types.ActionDefinition{}
}
description := ""
desc, err := a.i.Eval(fmt.Sprintf("%s.Description", a.config["name"]))
if err != nil {
xlog.Warn("No description found for custom action", "error", err, "action", a.config["name"])
} else {
d, ok := desc.Interface().(func() string)
if ok {
description = d()
}
}
if a.config["description"] != "" {
description = a.config["description"]
}
properties := v.Interface().(func() map[string][]string)
v, err = a.i.Eval(fmt.Sprintf("%s.RequiredFields", a.config["name"]))
@@ -125,7 +161,7 @@ func (a *CustomAction) Definition() types.ActionDefinition {
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(a.config["name"]),
Description: a.config["description"],
Description: description,
Properties: prop,
Required: requiredFields(),
}
@@ -159,5 +195,11 @@ func CustomConfigMeta() []config.Field {
Type: config.FieldTypeCheckbox,
HelpText: "Allow unsafe code execution",
},
{
Name: "configuration",
Label: "Configuration",
Type: config.FieldTypeTextarea,
HelpText: "Configuration for the custom action",
},
}
}
+1 -1
View File
@@ -76,7 +76,7 @@ return []string{"foo"}
Description: "A test action",
}))
runResult, err := customAction.Run(context.Background(), types.ActionParams{
runResult, err := customAction.Run(context.Background(), nil, types.ActionParams{
"Foo": "bar",
})
Expect(err).ToNot(HaveOccurred())
-48
View File
@@ -1,48 +0,0 @@
package action
import (
"context"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai/jsonschema"
)
// NewGoal creates a new intention action
// The inention action is special as it tries to identify
// a tool to use and a reasoning over to use it
func NewGoal() *GoalAction {
return &GoalAction{}
}
type GoalAction struct {
}
type GoalResponse struct {
Goal string `json:"goal"`
Achieved bool `json:"achieved"`
}
func (a *GoalAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil
}
func (a *GoalAction) Plannable() bool {
return false
}
func (a *GoalAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: "goal",
Description: "Check if the goal is achieved",
Properties: map[string]jsonschema.Definition{
"goal": {
Type: jsonschema.String,
Description: "The goal to check if it is achieved.",
},
"achieved": {
Type: jsonschema.Boolean,
Description: "Whether the goal is achieved",
},
},
Required: []string{"goal", "achieved"},
}
}
-50
View File
@@ -1,50 +0,0 @@
package action
import (
"context"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai/jsonschema"
)
// NewIntention creates a new intention action
// The inention action is special as it tries to identify
// a tool to use and a reasoning over to use it
func NewIntention(s ...string) *IntentAction {
return &IntentAction{tools: s}
}
type IntentAction struct {
tools []string
}
type IntentResponse struct {
Tool string `json:"tool"`
Reasoning string `json:"reasoning"`
}
func (a *IntentAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil
}
func (a *IntentAction) Plannable() bool {
return false
}
func (a *IntentAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: "pick_tool",
Description: "Pick a tool",
Properties: map[string]jsonschema.Definition{
"reasoning": {
Type: jsonschema.String,
Description: "A detailed reasoning on why you want to call this tool.",
},
"tool": {
Type: jsonschema.String,
Description: "The tool you want to use",
Enum: a.tools,
},
},
Required: []string{"tool", "reasoning"},
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ import (
"github.com/sashabaranov/go-openai/jsonschema"
)
const ConversationActionName = "new_conversation"
const ConversationActionName = "send_message"
func NewConversation() *ConversationAction {
return &ConversationAction{}
@@ -19,7 +19,7 @@ type ConversationActionResponse struct {
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
}
+1 -1
View File
@@ -16,7 +16,7 @@ func NewStop() *StopAction {
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
}
-71
View File
@@ -1,71 +0,0 @@
package action
import (
"context"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai/jsonschema"
)
// PlanActionName is the name of the plan action
// used by the LLM to schedule more actions
const PlanActionName = "plan"
func NewPlan(plannableActions []string) *PlanAction {
return &PlanAction{
plannables: plannableActions,
}
}
type PlanAction struct {
plannables []string
}
type PlanResult struct {
Subtasks []PlanSubtask `json:"subtasks"`
Goal string `json:"goal"`
}
type PlanSubtask struct {
Action string `json:"action"`
Reasoning string `json:"reasoning"`
}
func (a *PlanAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil
}
func (a *PlanAction) Plannable() bool {
return false
}
func (a *PlanAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: PlanActionName,
Description: "Use it for situations that involves doing more actions in sequence.",
Properties: map[string]jsonschema.Definition{
"subtasks": {
Type: jsonschema.Array,
Description: "The subtasks to be executed",
Items: &jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"action": {
Type: jsonschema.String,
Description: "The action to call",
Enum: a.plannables,
},
"reasoning": {
Type: jsonschema.String,
Description: "The reasoning for calling this action",
},
},
},
},
"goal": {
Type: jsonschema.String,
Description: "The goal of this plan",
},
},
Required: []string{"subtasks", "goal"},
}
}
-43
View File
@@ -1,43 +0,0 @@
package action
import (
"context"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai/jsonschema"
)
// NewReasoning creates a new reasoning action
// The reasoning action is special as it tries to force the LLM
// to think about what to do next
func NewReasoning() *ReasoningAction {
return &ReasoningAction{}
}
type ReasoningAction struct{}
type ReasoningResponse struct {
Reasoning string `json:"reasoning"`
}
func (a *ReasoningAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil
}
func (a *ReasoningAction) Plannable() bool {
return false
}
func (a *ReasoningAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: "pick_action",
Description: "try to understand what's the best thing to do and pick an action with a reasoning",
Properties: map[string]jsonschema.Definition{
"reasoning": {
Type: jsonschema.String,
Description: "A detailed reasoning on what would you do in this situation.",
},
},
Required: []string{"reasoning"},
}
}
+268
View File
@@ -0,0 +1,268 @@
package action
import (
"context"
"fmt"
"strings"
"time"
"github.com/mudler/LocalAGI/core/scheduler"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai/jsonschema"
)
const (
RecurringReminderActionName = "set_recurring_task"
OneTimeReminderActionName = "set_onetime_task"
ListRemindersName = "list_tasks"
RemoveReminderName = "remove_task"
)
func NewRecurringReminder() *RecurringReminderAction {
return &RecurringReminderAction{}
}
func NewOneTimeReminder() *OneTimeReminderAction {
return &OneTimeReminderAction{}
}
func NewListReminders() *ListRemindersAction {
return &ListRemindersAction{}
}
func NewRemoveReminder() *RemoveReminderAction {
return &RemoveReminderAction{}
}
type RecurringReminderAction struct{}
type OneTimeReminderAction struct{}
type ListRemindersAction struct{}
type RemoveReminderAction struct{}
type RemoveReminderParams struct {
Index int `json:"index"`
}
func (a *RecurringReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := types.RecurringReminderParams{}
err := params.Unmarshal(&result)
if err != nil {
return types.ActionResult{}, err
}
task, err := scheduler.NewTask(
sharedState.AgentName,
result.Message,
scheduler.ScheduleTypeCron,
result.CronExpr,
)
if err != nil {
return types.ActionResult{}, err
}
task.Metadata["reminder_type"] = "user_created"
err = sharedState.Scheduler.CreateTask(task)
if err != nil {
return types.ActionResult{}, err
}
return types.ActionResult{
Result: fmt.Sprintf("Recurring reminder set successfully (ID: %s). Next run: %s", task.ID, task.NextRun.Format(time.RFC3339)),
Metadata: map[string]interface{}{
"task_id": task.ID,
"message": result.Message,
"next_run": task.NextRun,
},
}, nil
}
func (a *OneTimeReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := types.OneTimeReminderParams{}
err := params.Unmarshal(&result)
if err != nil {
return types.ActionResult{}, err
}
// Validate the delay parses correctly before creating the task
_, err = scheduler.ParseDuration(result.Delay)
if err != nil {
return types.ActionResult{}, fmt.Errorf("invalid delay format, expected a duration like '30m', '2h', '1d', '1d12h': %w", err)
}
task, err := scheduler.NewTask(
sharedState.AgentName,
result.Message,
scheduler.ScheduleTypeOnce,
result.Delay,
)
if err != nil {
return types.ActionResult{}, err
}
task.Metadata["reminder_type"] = "user_created"
err = sharedState.Scheduler.CreateTask(task)
if err != nil {
return types.ActionResult{}, err
}
return types.ActionResult{
Result: fmt.Sprintf("One-time reminder set in %s (at %s, ID: %s)", result.Delay, task.NextRun.Format(time.RFC3339), task.ID),
Metadata: map[string]interface{}{
"task_id": task.ID,
"message": result.Message,
"next_run": task.NextRun,
},
}, nil
}
func (a *ListRemindersAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
tasks, err := sharedState.Scheduler.GetAllTasks()
if err != nil {
return types.ActionResult{}, err
}
if len(tasks) == 0 {
return types.ActionResult{
Result: "No reminders set",
}, nil
}
var result strings.Builder
result.WriteString("Current reminders:\n")
for i, task := range tasks {
status := "one-time"
if task.ScheduleType == scheduler.ScheduleTypeCron || task.ScheduleType == scheduler.ScheduleTypeInterval {
status = "recurring"
}
result.WriteString(fmt.Sprintf("%d. %s (Next run: %s, Status: %s, ID: %s)\n",
i+1,
task.Prompt,
task.NextRun.Format(time.RFC3339),
status,
task.ID))
}
return types.ActionResult{
Result: result.String(),
Metadata: map[string]interface{}{
"tasks": tasks,
},
}, nil
}
func (a *RemoveReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
var removeParams RemoveReminderParams
err := params.Unmarshal(&removeParams)
if err != nil {
return types.ActionResult{}, err
}
tasks, err := sharedState.Scheduler.GetAllTasks()
if err != nil {
return types.ActionResult{}, err
}
if len(tasks) == 0 {
return types.ActionResult{
Result: "No reminders to remove",
}, nil
}
// Convert from 1-based index to 0-based
index := removeParams.Index - 1
if index < 0 || index >= len(tasks) {
return types.ActionResult{}, fmt.Errorf("invalid reminder index: %d", removeParams.Index)
}
task := tasks[index]
err = sharedState.Scheduler.DeleteTask(task.ID)
if err != nil {
return types.ActionResult{}, err
}
return types.ActionResult{
Result: fmt.Sprintf("Removed reminder: %s", task.Prompt),
Metadata: map[string]interface{}{
"removed_task_id": task.ID,
},
}, nil
}
func (a *RecurringReminderAction) Plannable() bool {
return true
}
func (a *OneTimeReminderAction) Plannable() bool {
return true
}
func (a *ListRemindersAction) Plannable() bool {
return true
}
func (a *RemoveReminderAction) Plannable() bool {
return true
}
func (a *RecurringReminderAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: RecurringReminderActionName,
Description: "Set a recurring reminder for the agent to wake up and perform a task on a cron schedule. The reminder will keep repeating. Examples: '0 0 * * *' (daily at midnight), '0 */2 * * *' (every 2 hours), '0 0 * * 1' (every Monday at midnight)",
Properties: map[string]jsonschema.Definition{
"message": {
Type: jsonschema.String,
Description: "The message or task to be reminded about",
},
"cron_expr": {
Type: jsonschema.String,
Description: "Cron expression for scheduling (e.g. '0 0 * * *' for daily at midnight). Format: 'minute hour day month weekday'",
},
},
Required: []string{"message", "cron_expr"},
}
}
func (a *OneTimeReminderAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: OneTimeReminderActionName,
Description: "Set a one-time reminder for the agent to wake up and perform a task after a delay. The reminder triggers only once and is then automatically removed. Use this when asked to do something 'in X minutes/hours/days'. Examples: '30m' (30 minutes), '2h' (2 hours), '1d' (1 day), '1d12h' (1.5 days), '2h30m' (2.5 hours)",
Properties: map[string]jsonschema.Definition{
"message": {
Type: jsonschema.String,
Description: "The message or task to be reminded about",
},
"delay": {
Type: jsonschema.String,
Description: "How long to wait before triggering. Use Go duration format: '30m' (30 minutes), '2h' (2 hours), '1d' (1 day), '1d12h' (1.5 days), '2h30m' (2.5 hours)",
},
},
Required: []string{"message", "delay"},
}
}
func (a *ListRemindersAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: ListRemindersName,
Description: "List all currently set reminders with their next scheduled run times",
Properties: map[string]jsonschema.Definition{},
Required: []string{},
}
}
func (a *RemoveReminderAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: RemoveReminderName,
Description: "Remove a reminder by its index number (use list_reminders to see the index)",
Properties: map[string]jsonschema.Definition{
"index": {
Type: jsonschema.Integer,
Description: "The index number of the reminder to remove (1-based)",
},
},
Required: []string{"index"},
}
}
-45
View File
@@ -1,45 +0,0 @@
package action
import (
"context"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai/jsonschema"
)
// ReplyActionName is the name of the reply action
// used by the LLM to reply to the user without
// any additional processing
const ReplyActionName = "reply"
func NewReply() *ReplyAction {
return &ReplyAction{}
}
type ReplyAction struct{}
type ReplyResponse struct {
Message string `json:"message"`
}
func (a *ReplyAction) Run(context.Context, types.ActionParams) (string, error) {
return "no-op", nil
}
func (a *ReplyAction) Plannable() bool {
return false
}
func (a *ReplyAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: ReplyActionName,
Description: "Use this tool to reply to the user once we have all the informations we need.",
Properties: map[string]jsonschema.Definition{
"message": {
Type: jsonschema.String,
Description: "The message to reply with",
},
},
Required: []string{"message"},
}
}
+1 -40
View File
@@ -2,7 +2,6 @@ package action
import (
"context"
"fmt"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai/jsonschema"
@@ -16,25 +15,7 @@ func NewState() *StateAction {
type StateAction struct{}
// State is the structure
// that is used to keep track of the current state
// and the Agent's short memory that it can update
// Besides a long term memory that is accessible by the agent (With vector database),
// And a context memory (that is always powered by a vector database),
// this memory is the shorter one that the LLM keeps across conversation and across its
// reasoning process's and life time.
// TODO: A special action is then used to let the LLM itself update its memory
// periodically during self-processing, and the same action is ALSO exposed
// during the conversation to let the user put for example, a new goal to the agent.
type AgentInternalState struct {
NowDoing string `json:"doing_now"`
DoingNext string `json:"doing_next"`
DoneHistory []string `json:"done_history"`
Memories []string `json:"memories"`
Goal string `json:"goal"`
}
func (a *StateAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
func (a *StateAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
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,
)
}
+59 -383
View File
@@ -1,93 +1,19 @@
package agent
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/mudler/LocalAGI/core/action"
"github.com/mudler/LocalAGI/core/types"
"golang.org/x/exp/slices"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/xlog"
"github.com/sashabaranov/go-openai"
)
type decisionResult struct {
actionParams types.ActionParams
message string
actioName string
}
// decision forces the agent to take one of the available actions
func (a *Agent) decision(
ctx context.Context,
conversation []openai.ChatCompletionMessage,
tools []openai.Tool, toolchoice string, maxRetries int) (*decisionResult, error) {
var choice *openai.ToolChoice
if toolchoice != "" {
choice = &openai.ToolChoice{
Type: openai.ToolTypeFunction,
Function: openai.ToolFunction{Name: toolchoice},
}
}
var lastErr error
for attempts := 0; attempts < maxRetries; attempts++ {
decision := openai.ChatCompletionRequest{
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 {
lastErr = err
xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", err)
continue
}
jsonResp, _ := json.Marshal(resp)
xlog.Debug("Decision response", "response", string(jsonResp))
if len(resp.Choices) != 1 {
lastErr = fmt.Errorf("no choices: %d", len(resp.Choices))
xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", lastErr)
continue
}
msg := resp.Choices[0].Message
if len(msg.ToolCalls) != 1 {
if err := a.saveConversation(append(conversation, msg), "decision"); err != nil {
xlog.Error("Error saving conversation", "error", err)
}
return &decisionResult{message: msg.Content}, nil
}
params := types.ActionParams{}
if err := params.Read(msg.ToolCalls[0].Function.Arguments); err != nil {
lastErr = err
xlog.Warn("Attempt to parse action parameters failed", "attempt", attempts+1, "error", err)
continue
}
if err := a.saveConversation(append(conversation, msg), "decision"); err != nil {
xlog.Error("Error saving conversation", "error", err)
}
return &decisionResult{actionParams: params, actioName: msg.ToolCalls[0].Function.Name, message: msg.Content}, nil
}
return nil, fmt.Errorf("failed to make a decision after %d attempts: %w", maxRetries, lastErr)
}
type Messages []openai.ChatCompletionMessage
func (m Messages) ToOpenAI() []openai.ChatCompletionMessage {
@@ -155,6 +81,7 @@ func (m Messages) Save(path string) error {
}
func (m Messages) GetLatestUserMessage() *openai.ChatCompletionMessage {
xlog.Debug("Getting latest user message", "messages", m)
for i := len(m) - 1; i >= 0; i-- {
msg := m[i]
if msg.Role == UserRole {
@@ -165,179 +92,64 @@ func (m Messages) GetLatestUserMessage() *openai.ChatCompletionMessage {
return nil
}
func (m Messages) IsLastMessageFromRole(role string) bool {
if len(m) == 0 {
return false
}
return m[len(m)-1].Role == role
}
func (a *Agent) generateParameters(ctx context.Context, pickTemplate string, act types.Action, c []openai.ChatCompletionMessage, reasoning string, maxAttempts int) (*decisionResult, error) {
stateHUD, err := renderTemplate(pickTemplate, a.prepareHUD(), a.availableActions(), reasoning)
if err != nil {
return nil, err
}
conversation := c
if !Messages(c).Exist(stateHUD) && a.options.enableHUD {
conversation = append([]openai.ChatCompletionMessage{
{
Role: "system",
Content: stateHUD,
},
}, conversation...)
}
cc := conversation
if a.options.forceReasoning {
cc = append(conversation, openai.ChatCompletionMessage{
Role: "system",
Content: fmt.Sprintf("The agent decided to use the tool %s with the following reasoning: %s", act.Definition().Name, reasoning),
})
}
var result *decisionResult
var attemptErr error
for attempts := 0; attempts < maxAttempts; attempts++ {
result, attemptErr = a.decision(ctx,
cc,
a.availableActions().ToTools(),
act.Definition().Name.String(),
maxAttempts,
)
if attemptErr == nil && result.actionParams != nil {
return result, nil
// mergeLeadingSystemMessages replaces all leading system messages with a single
// system message. prefixBlocks are prepended in order (e.g. self-eval, then HUD).
// Only non-empty prefixBlocks are joined. Mid-conversation system messages are unchanged.
func (conv Messages) mergeLeadingSystemMessages(prefixBlocks ...string) Messages {
var leading []string
for _, s := range prefixBlocks {
if s != "" {
leading = append(leading, s)
}
xlog.Warn("Attempt to generate parameters failed", "attempt", attempts+1, "error", attemptErr)
}
return nil, fmt.Errorf("failed to generate parameters after %d attempts: %w", maxAttempts, attemptErr)
}
func (a *Agent) handlePlanning(ctx context.Context, job *types.Job, chosenAction types.Action, actionParams types.ActionParams, reasoning string, pickTemplate string, conv Messages) (Messages, error) {
// Planning: run all the actions in sequence
if !chosenAction.Definition().Name.Is(action.PlanActionName) {
xlog.Debug("no plan action")
return conv, nil
}
xlog.Debug("[planning]...")
planResult := action.PlanResult{}
if err := actionParams.Unmarshal(&planResult); err != nil {
return conv, fmt.Errorf("error unmarshalling plan result: %w", err)
}
stateResult := types.ActionState{
ActionCurrentState: types.ActionCurrentState{
Job: job,
Action: chosenAction,
Params: actionParams,
Reasoning: reasoning,
},
ActionResult: types.ActionResult{
Result: fmt.Sprintf("planning %s, subtasks: %+v", planResult.Goal, planResult.Subtasks),
},
}
job.Result.SetResult(stateResult)
job.CallbackWithResult(stateResult)
xlog.Info("[Planning] starts", "agent", a.Character.Name, "goal", planResult.Goal)
for _, s := range planResult.Subtasks {
xlog.Info("[Planning] subtask", "agent", a.Character.Name, "action", s.Action, "reasoning", s.Reasoning)
}
if len(planResult.Subtasks) == 0 {
return conv, fmt.Errorf("no subtasks")
}
// Execute all subtasks in sequence
for _, subtask := range planResult.Subtasks {
xlog.Info("[subtask] Generating parameters",
"agent", a.Character.Name,
"action", subtask.Action,
"reasoning", reasoning,
)
subTaskAction := a.availableActions().Find(subtask.Action)
subTaskReasoning := fmt.Sprintf("%s Overall goal is: %s", subtask.Reasoning, planResult.Goal)
params, err := a.generateParameters(ctx, pickTemplate, subTaskAction, conv, subTaskReasoning, maxRetries)
if err != nil {
xlog.Error("error generating action's parameters", "error", err)
return conv, fmt.Errorf("error generating action's parameters: %w", err)
}
actionParams = params.actionParams
if !job.Callback(types.ActionCurrentState{
Job: job,
Action: subTaskAction,
Params: actionParams,
Reasoning: subTaskReasoning,
}) {
job.Result.SetResult(types.ActionState{
ActionCurrentState: types.ActionCurrentState{
Job: job,
Action: chosenAction,
Params: actionParams,
Reasoning: subTaskReasoning,
},
ActionResult: types.ActionResult{
Result: "stopped by callback",
},
})
job.Result.Conversation = conv
job.Result.Finish(nil)
break
}
result, err := a.runAction(ctx, subTaskAction, actionParams)
if err != nil {
xlog.Error("error running action", "error", err)
return conv, fmt.Errorf("error running action: %w", err)
}
stateResult := types.ActionState{
ActionCurrentState: types.ActionCurrentState{
Job: job,
Action: subTaskAction,
Params: actionParams,
Reasoning: subTaskReasoning,
},
ActionResult: result,
}
job.Result.SetResult(stateResult)
job.CallbackWithResult(stateResult)
xlog.Debug("[subtask] Action executed", "agent", a.Character.Name, "action", subTaskAction.Definition().Name, "result", result)
conv = a.addFunctionResultToConversation(subTaskAction, actionParams, result, conv)
}
return conv, nil
}
func (a *Agent) availableActions() types.Actions {
// defaultActions := append(a.options.userActions, action.NewReply())
addPlanAction := func(actions types.Actions) types.Actions {
if !a.options.canPlan {
return actions
}
plannablesActions := []string{}
for _, a := range actions {
if a.Plannable() {
plannablesActions = append(plannablesActions, a.Definition().Name.String())
i := 0
for i < len(conv) && conv[i].Role == SystemRole {
content := conv[i].Content
if content == "" && conv[i].MultiContent != nil {
for _, part := range conv[i].MultiContent {
if part.Type == openai.ChatMessagePartTypeText && part.Text != "" {
content = part.Text
break
}
}
}
planAction := action.NewPlan(plannablesActions)
actions = append(actions, planAction)
return actions
if content != "" {
leading = append(leading, content)
}
i++
}
if len(leading) == 0 {
return conv
}
combined := strings.Join(leading, "\n\n")
single := openai.ChatCompletionMessage{
Role: SystemRole,
Content: combined,
}
return append([]openai.ChatCompletionMessage{single}, conv[i:]...)
}
// getAvailableActionsForJob returns available actions including user-defined ones for a specific job
func (a *Agent) getAvailableActionsForJob(job *types.Job) types.Actions {
// Start with regular available actions
baseActions := a.availableActions(job)
// Add user-defined actions from the job
userTools := job.GetUserTools()
if len(userTools) > 0 {
userDefinedActions := types.CreateUserDefinedActions(userTools)
baseActions = append(baseActions, userDefinedActions...)
xlog.Debug("Added user-defined actions", "definitions", userTools)
}
defaultActions := append(a.mcpActions, a.options.userActions...)
return baseActions
}
if a.options.initiateConversations && a.selfEvaluationInProgress { // && self-evaluation..
func (a *Agent) availableActions(j *types.Job) types.Actions {
// defaultActions := append(a.options.userActions, action.NewReply())
defaultActions := slices.Clone(a.options.userActions)
if j.Metadata["type"] == "scheduled" || (a.options.initiateConversations && a.selfEvaluationInProgress) { // && self-evaluation..
acts := append(defaultActions, action.NewConversation())
if a.options.enableHUD {
acts = append(acts, action.NewState())
@@ -346,7 +158,7 @@ func (a *Agent) availableActions() types.Actions {
// acts = append(acts, action.NewStop())
// }
return addPlanAction(acts)
return acts
}
if a.options.canStopItself {
@@ -354,14 +166,14 @@ func (a *Agent) availableActions() types.Actions {
if a.options.enableHUD {
acts = append(acts, action.NewState())
}
return addPlanAction(acts)
return acts
}
if a.options.enableHUD {
return addPlanAction(append(defaultActions, action.NewState()))
return append(defaultActions, action.NewState())
}
return addPlanAction(defaultActions)
return defaultActions
}
func (a *Agent) prepareHUD() (promptHUD *PromptHUD) {
@@ -376,139 +188,3 @@ func (a *Agent) prepareHUD() (promptHUD *PromptHUD) {
ShowCharacter: a.options.showCharacter,
}
}
// 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) {
c := messages
xlog.Debug("[pickAction] picking action starts", "messages", messages)
// Identify the goal of this conversation
if !a.options.forceReasoning {
xlog.Debug("not forcing reasoning")
// We also could avoid to use functions here and get just a reply from the LLM
// and then use the reply to get the action
thought, err := a.decision(ctx,
messages,
a.availableActions().ToTools(),
"",
maxRetries)
if err != nil {
return nil, nil, "", err
}
xlog.Debug(fmt.Sprintf("thought action Name: %v", thought.actioName))
xlog.Debug(fmt.Sprintf("thought message: %v", thought.message))
// Find the action
chosenAction := a.availableActions().Find(thought.actioName)
if chosenAction == nil || thought.actioName == "" {
xlog.Debug("no answer")
// LLM replied with an answer?
//fmt.Errorf("no action found for intent:" + thought.actioName)
return nil, nil, thought.message, nil
}
xlog.Debug(fmt.Sprintf("chosenAction: %v", chosenAction.Definition().Name))
return chosenAction, thought.actionParams, thought.message, nil
}
xlog.Debug("[pickAction] forcing reasoning")
prompt, err := renderTemplate(templ, a.prepareHUD(), a.availableActions(), "")
if err != nil {
return nil, nil, "", err
}
// Get the LLM to think on what to do
// and have a thought
if !Messages(c).Exist(prompt) {
c = append([]openai.ChatCompletionMessage{
{
Role: "system",
Content: prompt,
},
}, c...)
}
thought, err := a.decision(ctx,
c,
types.Actions{action.NewReasoning()}.ToTools(),
action.NewReasoning().Definition().Name.String(), maxRetries)
if err != nil {
return nil, nil, "", err
}
originalReasoning := ""
response := &action.ReasoningResponse{}
if thought.actionParams != nil {
if err := thought.actionParams.Unmarshal(response); err != nil {
return nil, nil, "", err
}
originalReasoning = response.Reasoning
}
if thought.message != "" {
originalReasoning = thought.message
}
xlog.Debug("[pickAction] picking action", "messages", c)
// thought, err := a.askLLM(ctx,
// c,
actionsID := []string{"reply"}
for _, m := range a.availableActions() {
actionsID = append(actionsID, m.Definition().Name.String())
}
xlog.Debug("[pickAction] actionsID", "actionsID", actionsID)
intentionsTools := action.NewIntention(actionsID...)
// TODO: FORCE to select ana ction here
// NOTE: we do not give the full conversation here to pick the action
// to avoid hallucinations
// Extract an action
params, err := a.decision(ctx,
append(c, openai.ChatCompletionMessage{
Role: "system",
Content: "Pick the relevant action given the following reasoning: " + originalReasoning,
}),
types.Actions{intentionsTools}.ToTools(),
intentionsTools.Definition().Name.String(), maxRetries)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to get the action tool parameters: %v", err)
}
if params.actionParams == nil {
xlog.Debug("[pickAction] no action params found")
return nil, nil, params.message, nil
}
actionChoice := action.IntentResponse{}
err = params.actionParams.Unmarshal(&actionChoice)
if err != nil {
return nil, nil, "", err
}
if actionChoice.Tool == "" || actionChoice.Tool == "reply" {
xlog.Debug("[pickAction] no action found, replying")
return nil, nil, "", nil
}
chosenAction := a.availableActions().Find(actionChoice.Tool)
xlog.Debug("[pickAction] chosenAction", "chosenAction", chosenAction, "actionName", actionChoice.Tool)
// // Let's double check if the action is correct by asking the LLM to judge it
// if chosenAction!= nil {
// promptString:= "Given the following goal and thoughts, is the action correct? \n\n"
// promptString+= fmt.Sprintf("Goal: %s\n", goalResponse.Goal)
// promptString+= fmt.Sprintf("Thoughts: %s\n", originalReasoning)
// promptString+= fmt.Sprintf("Action: %s\n", chosenAction.Definition().Name.String())
// promptString+= fmt.Sprintf("Action description: %s\n", chosenAction.Definition().Description)
// promptString+= fmt.Sprintf("Action parameters: %s\n", params.actionParams)
// }
return chosenAction, nil, originalReasoning, nil
}
+1155 -544
View File
File diff suppressed because it is too large Load Diff
+48 -13
View File
@@ -7,8 +7,8 @@ import (
"strings"
"sync"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/LocalAGI/services/actions"
"github.com/mudler/xlog"
. "github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/core/types"
@@ -37,14 +37,15 @@ var debugOptions = []types.JobOption{
}
type TestAction struct {
response map[string]string
response map[string]string
definition *types.ActionDefinition
}
func (a *TestAction) Plannable() bool {
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 {
if strings.Contains(strings.ToLower(p.String()), strings.ToLower(k)) {
return types.ActionResult{Result: r}, nil
@@ -55,7 +56,7 @@ func (a *TestAction) Run(c context.Context, p types.ActionParams) (types.ActionR
}
func (a *TestAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
def := types.ActionDefinition{
Name: "get_weather",
Description: "get current weather",
Properties: map[string]jsonschema.Definition{
@@ -71,6 +72,11 @@ func (a *TestAction) Definition() types.ActionDefinition {
Required: []string{"location"},
}
if a.definition != nil {
def = *a.definition
}
return def
}
type FakeStoreResultAction struct {
@@ -128,7 +134,6 @@ var _ = Describe("Agent test", func() {
WithModel(testModel),
EnableForceReasoning,
WithTimeout("10m"),
WithLoopDetectionSteps(3),
// WithRandomIdentity(),
WithActions(&TestAction{response: map[string]string{
"boston": testActionResult,
@@ -153,6 +158,9 @@ var _ = Describe("Agent test", func() {
}
Expect(reasons).To(ContainElement(testActionResult), fmt.Sprint(res))
Expect(reasons).To(ContainElement(testActionResult2), fmt.Sprint(res))
Expect(len(res.Conversation)).To(BeNumerically(">", 1), fmt.Sprint(res.Conversation))
reasons = []string{}
res = agent.Ask(
@@ -225,8 +233,33 @@ var _ = Describe("Agent test", func() {
WithModel(testModel),
WithLLMAPIKey(apiKeyURL),
WithTimeout("10m"),
WithMaxEvaluationLoops(1),
WithActions(
actions.NewSearch(map[string]string{}),
&TestAction{
response: map[string]string{
"boston": testActionResult,
"milan": testActionResult2,
},
},
&TestAction{
response: map[string]string{
"flight": "Flight options from Boston to Milan (April 22-26, 2025):\n• Outbound: Boston Logan (BOS) → Milan Malpensa (MXP), April 22, 2025\n - Economy: $450-650 (Alitalia, Delta, Lufthansa)\n - Business: $1,200-1,800\n - Flight time: 8h 15m (1 stop) or 9h 45m (direct)\n• Return: Milan Malpensa (MXP) → Boston Logan (BOS), April 26, 2025\n - Economy: $420-580\n - Business: $1,100-1,600\n• Total estimated cost: $870-1,230 per person\n• Best booking window: 2-3 months in advance for optimal prices",
"hotel": "Hotel recommendations in Milan for April 22-26, 2025:\n• Luxury (4-5 stars): $200-400/night\n - Hotel Principe di Savoia: $380/night (central location)\n - Mandarin Oriental: $420/night (luxury amenities)\n• Mid-range (3-4 stars): $120-200/night\n - Hotel Spadari al Duomo: $160/night (near cathedral)\n - Hotel Milano Scala: $140/night (theater district)\n• Budget (2-3 stars): $80-120/night\n - Hotel Bernina: $95/night (near train station)\n• Total 4-night stay: $320-1,680 depending on category\n• Booking tip: Reserve early for spring season discounts",
"car": "Car rental options in Milan for April 22-26, 2025:\n• Economy cars: $35-50/day (Fiat 500, VW Polo)\n• Compact cars: $45-65/day (Ford Focus, Opel Astra)\n• Mid-size cars: $60-85/day (BMW 3 Series, Audi A4)\n• SUV/Luxury: $90-150/day (BMW X3, Mercedes E-Class)\n• Total 4-day rental: $140-600\n• Pickup locations: Milan Malpensa Airport, Milan Central Station, city center\n• Insurance: $15-25/day additional\n• Fuel: ~$60-80 for 4 days of city driving\n• Parking: $20-40/day in city center hotels",
"food": "Dining budget and recommendations for Milan (April 22-26, 2025):\n• Fine dining: $80-150/person (Michelin-starred restaurants)\n - Cracco: $120/person (2 Michelin stars)\n - Trussardi alla Scala: $100/person\n• Mid-range restaurants: $40-80/person\n - Luini: $15/person (famous panzerotti)\n - Piz: $25/person (authentic pizza)\n• Casual dining: $20-40/person\n - Aperitivo bars: $15-25/person\n - Street food: $8-15/person\n• Daily food budget: $60-120/person\n• Total 4-day food cost: $240-480/person\n• Must-try: Risotto alla Milanese, Osso Buco, Panettone",
},
definition: &types.ActionDefinition{
Name: "search_internet",
Description: "search the internet for information",
Properties: map[string]jsonschema.Definition{
"query": {
Type: jsonschema.String,
Description: "The query to search for",
},
},
Required: []string{"query"},
},
},
),
EnablePlanning,
EnableForceReasoning,
@@ -238,18 +271,20 @@ var _ = Describe("Agent test", func() {
defer agent.Stop()
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("Create a plan for my 4-day trip from Boston to milan in April of this year (2025). I'm not sure about the dates yet, I want you to find out the best dates also according to what you find."),
)
Expect(len(result.State)).To(BeNumerically(">", 1))
Expect(len(result.Conversation)).To(BeNumerically(">", 1), fmt.Sprint(result.Conversation))
Expect(len(result.Plans)).To(BeNumerically(">=", 1), fmt.Sprintf("%+v", result))
Expect(len(result.State)).To(BeNumerically(">=", 1))
actionsExecuted := []string{}
for _, r := range result.State {
xlog.Info(r.Result)
actionsExecuted = append(actionsExecuted, r.Action.Definition().Name.String())
}
Expect(actionsExecuted).To(ContainElement("search_internet"), fmt.Sprint(result))
Expect(actionsExecuted).To(ContainElement("plan"), fmt.Sprint(result))
Expect(actionsExecuted).To(Or(ContainElement("search_internet"), ContainElement("get_weather")), fmt.Sprint(result))
})
It("Can initiate conversations", func() {
@@ -261,9 +296,9 @@ var _ = Describe("Agent test", func() {
WithModel(testModel),
WithLLMAPIKey(apiKeyURL),
WithTimeout("10m"),
WithNewConversationSubscriber(func(m openai.ChatCompletionMessage) {
WithNewConversationSubscriber(func(m *types.ConversationMessage) {
mu.Lock()
message = m
message = m.Message
mu.Unlock()
}),
WithActions(
+1 -1
View File
@@ -12,7 +12,7 @@ func (a *Agent) generateIdentity(guidance string) error {
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)
a.Character = a.options.character
if err != nil {
+223 -26
View File
@@ -1,20 +1,42 @@
package agent
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/cogito"
"github.com/mudler/xlog"
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
)
func (a *Agent) knowledgeBaseLookup(conv Messages) {
if (!a.options.enableKB && !a.options.enableLongTermMemory && !a.options.enableSummaryMemory) ||
len(conv) <= 0 {
func (a *Agent) knowledgeBaseLookup(job *types.Job, conv Messages) Messages {
// Only run KB recall/lookup when KB is explicitly enabled; long-term/summary memory
// only affect saving in saveConversation, not this lookup.
if !a.options.enableKB || len(conv) <= 0 {
xlog.Debug("[Knowledge Base Lookup] Disabled, skipping", "agent", a.Character.Name)
return
return conv
}
if !a.options.kbAutoSearch {
xlog.Debug("[Knowledge Base Lookup] Auto search disabled, skipping", "agent", a.Character.Name)
return conv
}
if a.options.ragdb == nil {
xlog.Debug("[Knowledge Base Lookup] No RAG DB configured, skipping", "agent", a.Character.Name)
return conv
}
var obs *types.Observable
if job != nil && job.Obs != nil && a.observer != nil {
obs = a.observer.NewObservable()
obs.Name = "Recall"
obs.Icon = "database"
obs.ParentID = job.Obs.ID
a.observer.Update(*obs)
}
// Walk conversation from bottom to top, and find the first message of the user
@@ -25,17 +47,35 @@ func (a *Agent) knowledgeBaseLookup(conv Messages) {
if userMessage == "" {
xlog.Info("[Knowledge Base Lookup] No user message found in conversation", "agent", a.Character.Name)
return
if obs != nil {
obs.Completion = &types.Completion{
Error: "No user message found in conversation",
}
a.observer.Update(*obs)
}
return conv
}
results, err := a.options.ragdb.Search(userMessage, a.options.kbResults)
if err != nil {
xlog.Info("Error finding similar strings inside KB:", "error", err)
if obs != nil {
obs.AddProgress(types.Progress{
Error: fmt.Sprintf("Error searching knowledge base: %v", err),
})
a.observer.Update(*obs)
}
}
if len(results) == 0 {
xlog.Info("[Knowledge Base Lookup] No similar strings found in KB", "agent", a.Character.Name)
return
if obs != nil {
obs.Completion = &types.Completion{
ActionResult: "No similar strings found in knowledge base",
}
a.observer.Update(*obs)
}
return conv
}
formatResults := ""
@@ -44,17 +84,30 @@ func (a *Agent) knowledgeBaseLookup(conv Messages) {
}
xlog.Info("[Knowledge Base Lookup] Found similar strings in KB", "agent", a.Character.Name, "results", formatResults)
// conv = append(conv,
// openai.ChatCompletionMessage{
// Role: "system",
// Content: fmt.Sprintf("Given the user input you have the following in memory:\n%s", formatResults),
// },
// )
conv = append([]openai.ChatCompletionMessage{
{
Role: "system",
Content: fmt.Sprintf("Given the user input you have the following in memory:\n%s", formatResults),
}}, conv...)
if obs != nil {
obs.AddProgress(types.Progress{
ActionResult: fmt.Sprintf("Found %d results in knowledge base", len(results)),
})
a.observer.Update(*obs)
}
// Create the message to add to conversation
systemMessage := openai.ChatCompletionMessage{
Role: "system",
Content: fmt.Sprintf("Given the user input you have the following in memory:\n%s", formatResults),
}
// Add the message to the conversation
conv = append([]openai.ChatCompletionMessage{systemMessage}, conv...)
if obs != nil {
obs.Completion = &types.Completion{
Conversation: []openai.ChatCompletionMessage{systemMessage},
}
a.observer.Update(*obs)
}
return conv
}
func (a *Agent) saveConversation(m Messages, prefix string) error {
@@ -84,24 +137,168 @@ func (a *Agent) saveCurrentConversation(conv Messages) {
xlog.Info("Saving conversation", "agent", a.Character.Name, "conversation size", len(conv))
if a.options.enableSummaryMemory && len(conv) > 0 {
msg, err := a.askLLM(a.context.Context, []openai.ChatCompletionMessage{{
Role: "user",
Content: "Summarize the conversation below, keep the highlights as a bullet list:\n" + Messages(conv).String(),
}}, maxRetries)
fragment := cogito.NewEmptyFragment().AddStartMessage("user", "Summarize the conversation below, keep the highlights as a bullet list:\n"+Messages(conv).String())
fragment, err := a.llm.Ask(a.context.Context, fragment)
if err != nil {
xlog.Error("Error summarizing conversation", "error", err)
}
msg := fragment.LastMessage()
if err := a.options.ragdb.Store(msg.Content); err != nil {
xlog.Error("Error storing into memory", "error", err)
}
} else {
for _, message := range conv {
if message.Role == "user" {
if err := a.options.ragdb.Store(message.Content); err != nil {
xlog.Error("Error storing into memory", "error", err)
// Use the conversation storage mode to determine what to store
switch a.options.conversationStorageMode {
case StoreWholeConversation:
// Store the entire conversation as a single block
if len(conv) > 0 {
convStr := Messages(conv).String()
if err := a.options.ragdb.Store(convStr); err != nil {
xlog.Error("Error storing whole conversation into memory", "error", err)
}
}
case StoreUserAndAssistant:
// Store user and assistant messages separately
for _, message := range conv {
if message.Role == "user" || message.Role == "assistant" {
if err := a.options.ragdb.Store(message.Content); err != nil {
xlog.Error("Error storing message into memory", "error", err, "role", message.Role)
}
}
}
case StoreUserOnly:
fallthrough
default:
// Store only user messages (default behavior)
for _, message := range conv {
if message.Role == "user" {
if err := a.options.ragdb.Store(message.Content); err != nil {
xlog.Error("Error storing into memory", "error", err)
}
}
}
}
}
}
// KBWrapperActions wraps RAGDB functionality as actions
type KBWrapperActions struct {
ragdb RAGDB
kbResults int
}
type SearchKnowledgeBaseAction struct {
*KBWrapperActions
}
type AddToKnowledgeBaseAction struct {
*KBWrapperActions
}
// NewKBWrapperActions creates factory functions for KB wrapper actions
func NewKBWrapperActions(ragdb RAGDB, kbResults int) (*SearchKnowledgeBaseAction, *AddToKnowledgeBaseAction) {
wrapper := &KBWrapperActions{
ragdb: ragdb,
kbResults: kbResults,
}
return &SearchKnowledgeBaseAction{wrapper}, &AddToKnowledgeBaseAction{wrapper}
}
func (a *SearchKnowledgeBaseAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
if a.ragdb == nil {
return types.ActionResult{}, fmt.Errorf("knowledge base is not configured")
}
var req struct {
Query string `json:"query"`
}
if err := params.Unmarshal(&req); err != nil {
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
}
if req.Query == "" {
return types.ActionResult{}, fmt.Errorf("query cannot be empty")
}
results, err := a.ragdb.Search(req.Query, a.kbResults)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to search knowledge base: %w", err)
}
if len(results) == 0 {
return types.ActionResult{
Result: fmt.Sprintf("No results found for query: %q", req.Query),
}, nil
}
formatResults := ""
for i, r := range results {
formatResults += fmt.Sprintf("%d. %s\n", i+1, r)
}
return types.ActionResult{
Result: fmt.Sprintf("Found %d result(s) for query %q:\n%s", len(results), req.Query, formatResults),
Metadata: map[string]interface{}{
"query": req.Query,
"results": results,
"count": len(results),
},
}, nil
}
func (a *SearchKnowledgeBaseAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: types.ActionDefinitionName("search_memory"),
Description: "Search your memory for relevant information using a query string",
Properties: map[string]jsonschema.Definition{
"query": {
Type: jsonschema.String,
Description: "The search query to find relevant information in the knowledge base",
},
},
Required: []string{"query"},
}
}
func (a *AddToKnowledgeBaseAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
if a.ragdb == nil {
return types.ActionResult{}, fmt.Errorf("knowledge base is not configured")
}
var req struct {
Content string `json:"content"`
}
if err := params.Unmarshal(&req); err != nil {
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
}
if req.Content == "" {
return types.ActionResult{}, fmt.Errorf("content cannot be empty")
}
if err := a.ragdb.Store(req.Content); err != nil {
return types.ActionResult{}, fmt.Errorf("failed to store content in knowledge base: %w", err)
}
return types.ActionResult{
Result: "Successfully added content to knowledge base",
Metadata: map[string]interface{}{
"content": req.Content,
},
}, nil
}
func (a *AddToKnowledgeBaseAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: types.ActionDefinitionName("add_memory"),
Description: "Add new content to your memory for future retrieval",
Properties: map[string]jsonschema.Definition{
"content": {
Type: jsonschema.String,
Description: "The content to store in the knowledge base",
},
},
Required: []string{"content"},
}
}
+181 -101
View File
@@ -3,60 +3,48 @@ package agent
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"time"
mcp "github.com/metoro-io/mcp-golang"
"github.com/metoro-io/mcp-golang/transport/http"
"net/http"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/xlog"
"github.com/sashabaranov/go-openai/jsonschema"
)
var _ types.Action = &mcpAction{}
var _ types.Action = &mcpWrapperAction{}
type MCPServer struct {
URL string `json:"url"`
Token string `json:"token"`
}
type mcpAction struct {
mcpClient *mcp.Client
type MCPSTDIOServer struct {
Name string `json:"name,omitempty"`
Args []string `json:"args"`
Env []string `json:"env"`
Cmd string `json:"cmd"`
}
type mcpWrapperAction struct {
mcpClient *mcp.ClientSession
inputSchema ToolInputSchema
toolName string
toolDescription string
}
func (a *mcpAction) Plannable() bool {
return true
func (m *mcpWrapperAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
// We don't call the method here, it is used by cogito.
// We will just use these to have a list of actions that MCP server provides for resolving internal states
return types.ActionResult{Result: "MCP action called"}, fmt.Errorf("not implemented")
}
func (m *mcpAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
resp, err := m.mcpClient.CallTool(ctx, m.toolName, params)
if err != nil {
xlog.Error("Failed to call tool", "error", err.Error())
return types.ActionResult{}, err
}
xlog.Debug("MCP response", "response", resp)
textResult := ""
for _, c := range resp.Content {
switch c.Type {
case mcp.ContentTypeText:
textResult += c.TextContent.Text + "\n"
case mcp.ContentTypeImage:
xlog.Error("Image content not supported yet")
case mcp.ContentTypeEmbeddedResource:
xlog.Error("Embedded resource content not supported yet")
}
}
return types.ActionResult{
Result: textResult,
}, nil
}
func (m *mcpAction) Definition() types.ActionDefinition {
func (m *mcpWrapperAction) Definition() types.ActionDefinition {
props := map[string]jsonschema.Definition{}
dat, err := json.Marshal(m.inputSchema.Properties)
if err != nil {
@@ -79,86 +67,178 @@ type ToolInputSchema struct {
Required []string `json:"required,omitempty"`
}
func (a *Agent) initMCPActions() error {
func (a *Agent) addTools(client *mcp.ClientSession) (types.Actions, error) {
var generatedActions types.Actions
a.mcpActions = nil
tools, err := client.ListTools(a.context, nil)
if err != nil {
xlog.Error("Failed to list tools", "error", err.Error())
return nil, err
}
for _, t := range tools.Tools {
desc := ""
if t.Description != "" {
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, &mcpWrapperAction{
mcpClient: client,
toolName: t.Name,
inputSchema: inputSchema,
toolDescription: desc,
})
}
return generatedActions, nil
}
// bearerTokenRoundTripper is a custom roundtripper that injects a bearer token
// into HTTP requests
type bearerTokenRoundTripper struct {
token string
base http.RoundTripper
}
// RoundTrip implements the http.RoundTripper interface
func (rt *bearerTokenRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if rt.token != "" {
req.Header.Set("Authorization", "Bearer "+rt.token)
}
return rt.base.RoundTrip(req)
}
// newBearerTokenRoundTripper creates a new roundtripper that injects the given token
func newBearerTokenRoundTripper(token string, base http.RoundTripper) http.RoundTripper {
if base == nil {
base = http.DefaultTransport
}
return &bearerTokenRoundTripper{
token: token,
base: base,
}
}
func (a *Agent) initMCPActions() error {
a.closeMCPServers() // Make sure we stop all previous servers if any is active
a.mcpActionDefinitions = nil
var err error
generatedActions := types.Actions{}
client := mcp.NewClient(&mcp.Implementation{Name: "LocalAI", Version: "v1.0.0"}, nil)
// Connect to a server over stdin/stdout.
// MCP HTTP Servers
for _, mcpServer := range a.options.mcpServers {
transport := http.NewHTTPClientTransport("/mcp")
transport.WithBaseURL(mcpServer.URL)
if mcpServer.Token != "" {
transport.WithHeader("Authorization", "Bearer "+mcpServer.Token)
// Create HTTP client with custom roundtripper for bearer token injection
httpclient := &http.Client{
Timeout: 360 * time.Second,
Transport: newBearerTokenRoundTripper(mcpServer.Token, http.DefaultTransport),
}
// Create a new client
client := mcp.NewClient(transport)
streamableTransport := &mcp.StreamableClientTransport{HTTPClient: httpclient, Endpoint: mcpServer.URL}
session, err := client.Connect(a.context, streamableTransport, nil)
if err != nil {
xlog.Error("Failed to connect to MCP server via StreamableClientTransport", "server", mcpServer, "error", err.Error())
xlog.Debug("Initializing client", "server", mcpServer.URL)
// Initialize the client
response, e := client.Initialize(a.context)
if e != nil {
xlog.Error("Failed to initialize client", "error", e.Error(), "server", mcpServer)
if err == nil {
err = e
} else {
err = errors.Join(err, e)
}
continue
}
xlog.Debug("Client initialized: %v", response.Instructions)
var cursor *string
for {
tools, err := client.ListTools(a.context, cursor)
sseTransport := &mcp.SSEClientTransport{HTTPClient: httpclient, Endpoint: mcpServer.URL}
session, err = client.Connect(a.context, sseTransport, nil)
if err != nil {
xlog.Error("Failed to list tools", "error", err.Error())
return err
xlog.Error("Failed to connect to MCP server via SSEClientTransport", "server", mcpServer, "error", err.Error())
continue
}
for _, t := range tools.Tools {
desc := ""
if t.Description != nil {
desc = *t.Description
}
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
}
a.mcpSessions = append(a.mcpSessions, session)
xlog.Debug("Adding tools for MCP server", "server", mcpServer)
actions, err := a.addTools(session)
if err != nil {
xlog.Error("Failed to add tools for MCP server", "server", mcpServer, "error", err.Error())
}
generatedActions = append(generatedActions, actions...)
}
a.mcpActions = generatedActions
// MCP STDIO Servers
if a.options.mcpPrepareScript != "" {
xlog.Debug("Preparing MCP", "script", a.options.mcpPrepareScript)
prepareCmd := exec.Command("/bin/bash", "-c", a.options.mcpPrepareScript)
output, err := prepareCmd.CombinedOutput()
if err != nil {
xlog.Error("Failed with error: '%s' - %s", err.Error(), output)
}
xlog.Debug("Prepared MCP: \n%s", output)
}
for _, mcpStdioServer := range a.options.mcpStdioServers {
command := exec.Command(mcpStdioServer.Cmd, mcpStdioServer.Args...)
command.Env = os.Environ()
command.Env = append(command.Env, mcpStdioServer.Env...)
// Create a new client
session, err := client.Connect(a.context, &mcp.CommandTransport{
Command: command}, nil)
if err != nil {
xlog.Error("Failed to connect to MCP server", "server", mcpStdioServer, "error", err.Error())
continue
}
a.mcpSessions = append(a.mcpSessions, session)
xlog.Debug("Adding tools for MCP server (stdio)", "server", mcpStdioServer)
actions, err := a.addTools(session)
if err != nil {
xlog.Error("Failed to add tools for MCP server", "server", mcpStdioServer, "error", err.Error())
}
generatedActions = append(generatedActions, actions...)
}
// Pre-connected MCP sessions (e.g. in-process skills server); already in a.mcpSessions after closeMCPServers()
for _, session := range a.options.extraMCPSessions {
actions, err := a.addTools(session)
if err != nil {
xlog.Error("Failed to add tools for extra MCP session", "error", err.Error())
continue
}
a.mcpSessions = append(a.mcpSessions, session)
generatedActions = append(generatedActions, actions...)
}
a.mcpActionDefinitions = generatedActions
return err
}
func (a *Agent) closeMCPServers() {
extraSet := make(map[*mcp.ClientSession]bool)
for _, e := range a.options.extraMCPSessions {
extraSet[e] = true
}
var keep []*mcp.ClientSession
for _, s := range a.mcpSessions {
if extraSet[s] {
keep = append(keep, s)
} else {
s.Close()
}
}
a.mcpSessions = keep
}
+101
View File
@@ -0,0 +1,101 @@
package agent
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("mergeLeadingSystemMessages", func() {
It("merges multiple leading system messages into one", func() {
conv := Messages{
{Role: SystemRole, Content: "You are a helper."},
{Role: SystemRole, Content: "Given the user input you have the following in memory:\n- fact1"},
{Role: "user", Content: "hello"},
}
out := conv.mergeLeadingSystemMessages()
Expect(out).To(HaveLen(2))
Expect(out[0].Role).To(Equal(SystemRole))
Expect(out[0].Content).To(Equal("You are a helper.\n\nGiven the user input you have the following in memory:\n- fact1"))
Expect(out[1].Role).To(Equal("user"))
Expect(out[1].Content).To(Equal("hello"))
})
It("prepends prefix blocks in order (self-eval then HUD)", func() {
conv := Messages{
{Role: SystemRole, Content: "Main system prompt."},
{Role: "user", Content: "hi"},
}
out := conv.mergeLeadingSystemMessages("Self-eval block.", "HUD block.")
Expect(out).To(HaveLen(2))
Expect(out[0].Role).To(Equal(SystemRole))
Expect(out[0].Content).To(Equal("Self-eval block.\n\nHUD block.\n\nMain system prompt."))
Expect(out[1].Role).To(Equal("user"))
})
It("skips empty prefix blocks", func() {
conv := Messages{
{Role: SystemRole, Content: "Only this."},
{Role: "user", Content: "hi"},
}
out := conv.mergeLeadingSystemMessages("", "HUD.", "")
Expect(out[0].Content).To(Equal("HUD.\n\nOnly this."))
})
It("leaves mid-conversation system messages unchanged", func() {
conv := Messages{
{Role: SystemRole, Content: "Leading system."},
{Role: "user", Content: "message with images"},
{Role: SystemRole, Content: "Image explainer (would be rectified elsewhere)."},
{Role: "assistant", Content: "reply"},
}
out := conv.mergeLeadingSystemMessages()
Expect(out).To(HaveLen(4))
Expect(out[0].Role).To(Equal(SystemRole))
Expect(out[0].Content).To(Equal("Leading system."))
Expect(out[1].Role).To(Equal("user"))
Expect(out[2].Role).To(Equal(SystemRole))
Expect(out[2].Content).To(Equal("Image explainer (would be rectified elsewhere)."))
Expect(out[3].Role).To(Equal("assistant"))
})
It("returns conv unchanged when there are no leading system messages and no prefix blocks", func() {
conv := Messages{
{Role: "user", Content: "hi"},
}
out := conv.mergeLeadingSystemMessages()
Expect(out).To(Equal(conv))
})
It("returns only prefix blocks as single system message when conv has no leading system messages", func() {
conv := Messages{
{Role: "user", Content: "hi"},
}
out := conv.mergeLeadingSystemMessages("Self-eval.", "HUD.")
Expect(out).To(HaveLen(2))
Expect(out[0].Role).To(Equal(SystemRole))
Expect(out[0].Content).To(Equal("Self-eval.\n\nHUD."))
Expect(out[1].Role).To(Equal("user"))
})
It("produces exactly one leading system message with config + RAG + HUD content", func() {
conv := Messages{
{Role: SystemRole, Content: "RAG: memory context"},
{Role: SystemRole, Content: "Config system prompt."},
{Role: "user", Content: "hello"},
}
out := conv.mergeLeadingSystemMessages("Self-eval.", "HUD.")
Expect(out).To(HaveLen(2))
Expect(out[0].Role).To(Equal(SystemRole))
Expect(out[0].Content).To(ContainSubstring("Self-eval."))
Expect(out[0].Content).To(ContainSubstring("HUD."))
Expect(out[0].Content).To(ContainSubstring("RAG: memory context"))
Expect(out[0].Content).To(ContainSubstring("Config system prompt."))
systemCount := 0
for _, m := range out {
if m.Role == SystemRole {
systemCount++
}
}
Expect(systemCount).To(Equal(1))
})
})
+102
View File
@@ -0,0 +1,102 @@
package agent
import (
"encoding/json"
"sync"
"sync/atomic"
"github.com/mudler/LocalAGI/core/sse"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/xlog"
)
type Observer interface {
NewObservable() *types.Observable
Update(types.Observable)
History() []types.Observable
ClearHistory()
}
// historyRingSize is the number of observables kept in the ring buffer. When full,
// the oldest entry is overwritten. The UI builds a tree from parent_id; if a parent
// is evicted before its children, those children will appear as roots or be omitted.
const historyRingSize = 500
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, historyRingSize),
}
}
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
}
func (s *SSEObserver) ClearHistory() {
s.mutex.Lock()
defer s.mutex.Unlock()
s.history = make([]types.Observable, historyRingSize)
s.historyLast = 0
}
+300 -21
View File
@@ -5,17 +5,34 @@ import (
"strings"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai"
"github.com/mudler/cogito"
)
type Option func(*options) error
// ConversationStorageMode defines how conversations are stored in the knowledge base
type ConversationStorageMode string
const (
// StoreUserOnly stores only user messages (default)
StoreUserOnly ConversationStorageMode = "user_only"
// StoreUserAndAssistant stores both user and assistant messages separately
StoreUserAndAssistant ConversationStorageMode = "user_and_assistant"
// StoreWholeConversation stores the entire conversation as a single block
StoreWholeConversation ConversationStorageMode = "whole_conversation"
)
type llmOptions struct {
APIURL string
APIKey string
Model string
MultimodalModel string
APIURL string
APIKey string
Model string
MultimodalModel string
ReviewerModel string
TranscriptionModel string
TranscriptionLanguage string
TTSModel string
}
type options struct {
@@ -24,25 +41,41 @@ type options struct {
randomIdentityGuidance string
randomIdentity bool
userActions types.Actions
jobFilters types.JobFilters
enableHUD, standaloneJob, showCharacter, enableKB, enableSummaryMemory, enableLongTermMemory bool
stripThinkingTags bool
kbAutoSearch bool
conversationStorageMode ConversationStorageMode
canStopItself bool
initiateConversations bool
loopDetectionSteps int
forceReasoning bool
forceReasoningTool bool
enableGuidedTools bool
canPlan bool
disableSinkState bool
characterfile string
statefile string
schedulerStorePath string // Path to scheduler JSON storage file
context context.Context
permanentGoal string
timeout string
periodicRuns time.Duration
schedulerPollInterval time.Duration
kbResults int
ragdb RAGDB
// Evaluation settings
maxEvaluationLoops int
loopDetection int
enableEvaluation bool
prompts []DynamicPrompt
systemPrompt string
systemPrompt string
innerMonologueTemplate string
skillPromptTemplate string
schedulerTaskTemplate string
// callbacks
reasoningCallback func(types.ActionCurrentState) bool
@@ -50,9 +83,27 @@ type options struct {
conversationsPath string
mcpServers []MCPServer
mcpServers []MCPServer
mcpStdioServers []MCPSTDIOServer
mcpPrepareScript string
extraMCPSessions []*mcp.ClientSession
newConversationsSubscribers []func(*types.ConversationMessage)
newConversationsSubscribers []func(openai.ChatCompletionMessage)
observer Observer
enableAutoCompaction bool
autoCompactionThreshold int
parallelJobs int
lastMessageDuration time.Duration
// cancelPreviousOnNewMessage: when true (or nil), Enqueue cancels the running job for the same conversation_id. When false, jobs are queued.
cancelPreviousOnNewMessage *bool
// maxAttempts: on ExecuteTools failure, retry up to this many times before surfacing the error to the user (1 = no retries).
maxAttempts int
// streamCallback receives streaming events from cogito during final answer generation.
streamCallback func(cogito.StreamEvent)
}
func (o *options) SeparatedMultimodalModel() bool {
@@ -61,10 +112,20 @@ func (o *options) SeparatedMultimodalModel() bool {
func defaultOptions() *options {
return &options{
periodicRuns: 15 * time.Minute,
parallelJobs: 1,
maxAttempts: 1,
periodicRuns: 15 * time.Minute,
schedulerPollInterval: 30 * time.Second,
maxEvaluationLoops: 2,
enableEvaluation: false,
kbAutoSearch: true, // Default to true to maintain backward compatibility
conversationStorageMode: StoreUserOnly, // Default to user-only for backward compatibility
LLMAPI: llmOptions{
APIURL: "http://localhost:8080",
Model: "gpt-4",
APIURL: "http://localhost:8080",
Model: "gpt-4",
TranscriptionModel: "whisper-1",
TranscriptionLanguage: "",
TTSModel: "tts-1",
},
character: Character{
Name: "",
@@ -96,6 +157,16 @@ var EnableForceReasoning = func(o *options) error {
return nil
}
var EnableGuidedTools = func(o *options) error {
o.enableGuidedTools = true
return nil
}
var EnableForceReasoningTool = func(o *options) error {
o.forceReasoningTool = true
return nil
}
var EnableKnowledgeBase = func(o *options) error {
o.enableKB = true
o.kbResults = 5
@@ -114,13 +185,6 @@ func WithTimeout(timeout string) Option {
}
}
func WithLoopDetectionSteps(steps int) Option {
return func(o *options) error {
o.loopDetectionSteps = steps
return nil
}
}
func WithConversationsPath(path string) Option {
return func(o *options) error {
o.conversationsPath = path
@@ -136,7 +200,48 @@ func EnableKnowledgeBaseWithResults(results int) Option {
}
}
func WithNewConversationSubscriber(sub func(openai.ChatCompletionMessage)) 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
}
}
// WithCancelPreviousOnNewMessage sets whether a new job with the same conversation_id cancels the currently running job (true) or is queued (false). Nil/default means true.
func WithCancelPreviousOnNewMessage(cancel bool) Option {
return func(o *options) error {
o.cancelPreviousOnNewMessage = &cancel
return nil
}
}
// WithMaxAttempts sets how many times to attempt execution on failure before surfacing the error to the user (1 = no retries).
func WithMaxAttempts(attempts int) Option {
return func(o *options) error {
o.maxAttempts = attempts
return nil
}
}
func WithLoopDetection(loops int) Option {
return func(o *options) error {
o.loopDetection = loops
return nil
}
}
func WithNewConversationSubscriber(sub func(*types.ConversationMessage)) Option {
return func(o *options) error {
o.newConversationsSubscribers = append(o.newConversationsSubscribers, sub)
return nil
@@ -153,6 +258,18 @@ var EnablePlanning = func(o *options) error {
return nil
}
var DisableSinkState = func(o *options) error {
o.disableSinkState = true
return nil
}
var WithPlanReviewerLLM = func(model string) Option {
return func(o *options) error {
o.LLMAPI.ReviewerModel = model
return nil
}
}
// EnableStandaloneJob is an option to enable the agent
// to run jobs in the background automatically
var EnableStandaloneJob = func(o *options) error {
@@ -182,6 +299,19 @@ func WithRAGDB(db RAGDB) Option {
}
}
// WithConversationStorageMode sets how conversations are stored in the knowledge base
func WithConversationStorageMode(mode ConversationStorageMode) Option {
return func(o *options) error {
switch mode {
case StoreUserOnly, StoreUserAndAssistant, StoreWholeConversation:
o.conversationStorageMode = mode
default:
o.conversationStorageMode = StoreUserOnly
}
return nil
}
}
func WithSystemPrompt(prompt string) Option {
return func(o *options) error {
o.systemPrompt = prompt
@@ -189,6 +319,22 @@ func WithSystemPrompt(prompt string) Option {
}
}
// WithInnerMonologueTemplate sets the prompt used for periodic/standalone runs. If empty, the default template is used.
func WithInnerMonologueTemplate(template string) Option {
return func(o *options) error {
o.innerMonologueTemplate = template
return nil
}
}
// WithSkillPromptTemplate sets the template for rendering skills in the prompt. If empty, the default template is used.
func WithSkillPromptTemplate(template string) Option {
return func(o *options) error {
o.skillPromptTemplate = template
return nil
}
}
func WithMCPServers(servers ...MCPServer) Option {
return func(o *options) error {
o.mcpServers = servers
@@ -196,6 +342,20 @@ func WithMCPServers(servers ...MCPServer) Option {
}
}
func WithMCPSTDIOServers(servers ...MCPSTDIOServer) Option {
return func(o *options) error {
o.mcpStdioServers = servers
return nil
}
}
func WithMCPPrepareScript(script string) Option {
return func(o *options) error {
o.mcpPrepareScript = script
return nil
}
}
func WithLLMAPIURL(url string) Option {
return func(o *options) error {
o.LLMAPI.APIURL = url
@@ -227,6 +387,14 @@ func WithPrompts(prompts ...DynamicPrompt) Option {
}
}
// WithMCPSession adds a pre-connected MCP client session (e.g. in-process skills MCP) to the agent.
func WithMCPSession(session *mcp.ClientSession) Option {
return func(o *options) error {
o.extraMCPSessions = append(o.extraMCPSessions, session)
return nil
}
}
// WithDynamicPrompts is a helper function to create dynamic prompts
// Dynamic prompts contains golang code which is executed dynamically
// // to render a prompt to the LLM
@@ -275,6 +443,18 @@ func WithPeriodicRuns(duration string) Option {
}
}
func WithSchedulerPollInterval(duration string) Option {
return func(o *options) error {
t, err := time.ParseDuration(duration)
if err != nil {
o.schedulerPollInterval = 30 * time.Second
return nil
}
o.schedulerPollInterval = t
return nil
}
}
func WithContext(ctx context.Context) Option {
return func(o *options) error {
o.context = ctx
@@ -332,7 +512,106 @@ func WithRandomIdentity(guidance ...string) Option {
func WithActions(actions ...types.Action) Option {
return func(o *options) error {
o.userActions = actions
o.userActions = append(o.userActions, actions...)
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
}
}
func WithTranscriptionModel(model string) Option {
return func(o *options) error {
o.LLMAPI.TranscriptionModel = model
return nil
}
}
func WithTranscriptionLanguage(language string) Option {
return func(o *options) error {
o.LLMAPI.TranscriptionLanguage = language
return nil
}
}
func WithTTSModel(model string) Option {
return func(o *options) error {
o.LLMAPI.TTSModel = model
return nil
}
}
func WithKBAutoSearch(enabled bool) Option {
return func(o *options) error {
o.kbAutoSearch = enabled
return nil
}
}
// WithSchedulerStorePath sets the path for the scheduler's JSON storage file
func WithSchedulerStorePath(path string) Option {
return func(o *options) error {
o.schedulerStorePath = path
return nil
}
}
// WithSchedulerTaskTemplate sets the prompt used for scheduled/recurring tasks run by the scheduler.
// If empty, the default inner monologue template is used with the task injected.
func WithSchedulerTaskTemplate(template string) Option {
return func(o *options) error {
o.schedulerTaskTemplate = template
return nil
}
}
var EnableAutoCompaction = func(o *options) error {
o.enableAutoCompaction = true
return nil
}
func WithAutoCompactionThreshold(threshold int) Option {
return func(o *options) error {
o.autoCompactionThreshold = threshold
return nil
}
}
// WithStreamCallback sets a callback to receive streaming events from cogito
// during final answer generation. This enables live token-by-token delivery.
func WithStreamCallback(fn func(cogito.StreamEvent)) Option {
return func(o *options) error {
o.streamCallback = fn
return nil
}
}
+3 -1
View File
@@ -1,6 +1,8 @@
package agent
import "github.com/mudler/LocalAGI/core/types"
type DynamicPrompt interface {
Render(a *Agent) (string, error)
Render(a *Agent) (types.PromptResult, error)
Role() string
}
+84
View File
@@ -0,0 +1,84 @@
package agent
import (
"context"
"fmt"
"github.com/mudler/LocalAGI/core/scheduler"
"github.com/mudler/LocalAGI/core/types"
)
// agentSchedulerExecutor implements scheduler.AgentExecutor for executing scheduled tasks through the agent
type agentSchedulerExecutor struct {
agent *Agent
}
// Execute processes a scheduled task by creating a job for the agent
func (e *agentSchedulerExecutor) Execute(ctx context.Context, agentName string, prompt string) (*scheduler.JobResult, error) {
// Render the scheduler task template - if custom template is set, it will include {{.Task}}
// If no custom scheduler template is set, fall back to default inner monologue template
innerMonologue := fmt.Sprintf("You need to execute the following task, by using the tools available to you. When the task is completed, you need to send a message to the user with send_message tool to inform them that the task is completed: %s", prompt)
if e.agent.options.schedulerTaskTemplate != "" {
tmpl, err := templateBase("taskTemplate", e.agent.options.schedulerTaskTemplate)
if err != nil {
return nil, fmt.Errorf("failed to render scheduler task template: %w", err)
}
innerMonologue, err = templateExecute(tmpl, &InnerMonologueTemplateData{
CommonTemplateData: CommonTemplateData{
AgentName: agentName,
},
Task: prompt,
})
if err != nil {
return nil, fmt.Errorf("failed to render scheduler task template: %w", err)
}
}
// Create a job for the reminder with the rendered inner monologue
reminderJob := types.NewJob(
types.WithText(innerMonologue),
types.WithReasoningCallback(e.agent.options.reasoningCallback),
types.WithResultCallback(e.agent.options.resultCallback),
types.WithContext(ctx),
types.WithMetadata(map[string]any{
"message": prompt,
"is_reminder": true,
"type": "scheduled",
}),
)
// Attach observable so UI can show reminder processing state
if e.agent.observer != nil {
obs := e.agent.observer.NewObservable()
obs.Name = "reminder"
obs.Icon = "bell"
e.agent.observer.Update(*obs)
reminderJob.Obs = obs
}
// Send the job to be processed
e.agent.jobQueue <- reminderJob
// Wait for the job to complete or context to be cancelled
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
result, err := reminderJob.Result.WaitResult(ctx)
if err != nil {
return nil, err
}
if result.Error != nil {
return &scheduler.JobResult{
Response: "",
Error: result.Error,
}, result.Error
}
return &scheduler.JobResult{
Response: result.Response,
Error: nil,
}, nil
}
}
+6 -6
View File
@@ -6,7 +6,7 @@ import (
"os"
"path/filepath"
"github.com/mudler/LocalAGI/core/action"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai/jsonschema"
)
@@ -14,10 +14,10 @@ import (
// all information that should be displayed to the LLM
// in the prompts
type PromptHUD struct {
Character Character `json:"character"`
CurrentState action.AgentInternalState `json:"current_state"`
PermanentGoal string `json:"permanent_goal"`
ShowCharacter bool `json:"show_character"`
Character Character `json:"character"`
CurrentState types.AgentInternalState `json:"current_state"`
PermanentGoal string `json:"permanent_goal"`
ShowCharacter bool `json:"show_character"`
}
type Character struct {
@@ -80,7 +80,7 @@ func Load(path string) (*Character, error) {
return &c, nil
}
func (a *Agent) State() action.AgentInternalState {
func (a *Agent) State() types.AgentInternalState {
return *a.currentState
}
+1
View File
@@ -25,6 +25,7 @@ var _ = Describe("Agent test", func() {
agent, err = New(
WithLLMAPIURL(apiURL),
WithModel(testModel),
WithTimeout("10m"),
WithRandomIdentity(),
)
Expect(err).ToNot(HaveOccurred())
+28 -44
View File
@@ -2,18 +2,41 @@ package agent
import (
"bytes"
"html/template"
"text/template"
"time"
"github.com/Masterminds/sprig/v3"
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai"
)
type CommonTemplateData struct {
AgentName string
}
type InnerMonologueTemplateData struct {
CommonTemplateData
Task string
}
func templateBase(templateName, templatetext string) (*template.Template, error) {
return template.New(templateName).Funcs(sprig.FuncMap()).Parse(templatetext)
}
func templateExecute(template *template.Template, data interface{}) (string, error) {
prompt := bytes.NewBuffer([]byte{})
err := template.Execute(prompt, data)
if err != nil {
return "", err
}
return prompt.String(), nil
}
func renderTemplate(templ string, hud *PromptHUD, actions types.Actions, reasoning string) (string, error) {
// prepare the prompt
prompt := bytes.NewBuffer([]byte{})
promptTemplate, err := template.New("pickAction").Parse(templ)
promptTemplate, err := templateBase("pickAction", templ)
if err != nil {
return "", err
}
@@ -34,7 +57,7 @@ func renderTemplate(templ string, hud *PromptHUD, actions types.Actions, reasoni
Actions: definitions,
HUD: hud,
Reasoning: reasoning,
Time: time.Now().Format(time.RFC3339),
Time: time.Now().UTC().Format(time.RFC1123),
})
if err != nil {
return "", err
@@ -80,7 +103,8 @@ Current State:
- Current Goal: {{if .CurrentState.Goal}}{{.CurrentState.Goal}}{{else}}None{{end}}
- Action History: {{range .CurrentState.DoneHistory}}{{.}} {{end}}
- Short-term Memory: {{range .CurrentState.Memories}}{{.}} {{end}}{{end}}
Current Time: {{.Time}}`
Current Time and Date: {{.Time}}`
const pickSelfTemplate = `
You are an autonomous AI agent with a defined character and state (as shown above).
@@ -93,7 +117,6 @@ Guidelines:
4. Update your state appropriately
When making decisions:
- Use the "reply" tool to provide final responses
- Update your state using appropriate tools
- Plan complex tasks using the planning tool
- Consider both immediate and long-term goals
@@ -104,43 +127,4 @@ Remember:
- Keep track of your progress and state
- Be proactive in addressing potential issues
Available Tools:
{{range .Actions -}}
- {{.Name}}: {{.Description }}
{{ end }}
{{if .Reasoning}}Previous Reasoning: {{.Reasoning}}{{end}}
` + hudTemplate
const reSelfEvalTemplate = pickSelfTemplate
const pickActionTemplate = hudTemplate + `
Your only task is to analyze the conversation and determine a goal and the best tool to use, or just a final response if we have fullfilled the goal.
Guidelines:
1. Review the current state, what was done already and context
2. Consider available tools and their purposes
3. Plan your approach carefully
4. Explain your reasoning clearly
When choosing actions:
- Use "reply" or "answer" tools for direct responses
- Select appropriate tools for specific tasks
- Consider the impact of each action
- Plan for potential challenges
Decision Process:
1. Analyze the situation
2. Consider available options
3. Choose the best course of action
4. Explain your reasoning
5. Execute the chosen action
Available Tools:
{{range .Actions -}}
- {{.Name}}: {{.Description }}
{{ end }}
{{if .Reasoning}}Previous Reasoning: {{.Reasoning}}{{end}}`
const reEvalTemplate = pickActionTemplate
@@ -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")
}
@@ -1,11 +1,11 @@
package connectors
package conversations
import (
"fmt"
"sync"
"time"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/xlog"
"github.com/sashabaranov/go-openai"
)
@@ -1,9 +1,9 @@
package connectors_test
package conversations_test
import (
"time"
"github.com/mudler/LocalAGI/services/connectors"
"github.com/mudler/LocalAGI/core/conversations"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/sashabaranov/go-openai"
@@ -11,13 +11,13 @@ import (
var _ = Describe("ConversationTracker", func() {
var (
tracker *connectors.ConversationTracker[string]
tracker *conversations.ConversationTracker[string]
duration time.Duration
)
BeforeEach(func() {
duration = 1 * time.Second
tracker = connectors.NewConversationTracker[string](duration)
tracker = conversations.NewConversationTracker[string](duration)
})
It("should initialize with empty conversations", func() {
@@ -81,8 +81,8 @@ var _ = Describe("ConversationTracker", func() {
})
It("should handle different key types", func() {
trackerInt := connectors.NewConversationTracker[int](duration)
trackerInt64 := connectors.NewConversationTracker[int64](duration)
trackerInt := conversations.NewConversationTracker[int](duration)
trackerInt64 := conversations.NewConversationTracker[int64](duration)
message := openai.ChatCompletionMessage{
Role: openai.ChatMessageRoleUser,
+49
View File
@@ -0,0 +1,49 @@
package scheduler
import (
"context"
)
// TaskStore defines the interface for task persistence
type TaskStore interface {
// Create adds a new task
Create(task *Task) error
// Get retrieves a task by ID
Get(id string) (*Task, error)
// GetAll retrieves all tasks
GetAll() ([]*Task, error)
// GetDue retrieves tasks that are due for execution
GetDue() ([]*Task, error)
// GetByAgent retrieves all tasks for a specific agent
GetByAgent(agentName string) ([]*Task, error)
// Update updates an existing task
Update(task *Task) error
// Delete removes a task
Delete(id string) error
// LogRun records a task execution
LogRun(run *TaskRun) error
// GetRuns retrieves execution history for a task
GetRuns(taskID string, limit int) ([]*TaskRun, error)
// Close releases resources
Close() error
}
// AgentExecutor defines the interface for executing agent tasks
type AgentExecutor interface {
Execute(ctx context.Context, agentName string, prompt string) (*JobResult, error)
}
// JobResult represents the result of an agent execution
type JobResult struct {
Response string
Error error
}
+219
View File
@@ -0,0 +1,219 @@
package scheduler
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"time"
)
// JSONStore implements TaskStore using JSON file storage
type JSONStore struct {
filePath string
mu sync.RWMutex
data *storeData
}
type storeData struct {
Tasks []*Task `json:"tasks"`
TaskRuns []*TaskRun `json:"task_runs"`
}
// NewJSONStore creates a new JSON-based task store
func NewJSONStore(filePath string) (*JSONStore, error) {
store := &JSONStore{
filePath: filePath,
data: &storeData{
Tasks: make([]*Task, 0),
TaskRuns: make([]*TaskRun, 0),
},
}
if err := store.load(); err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("failed to load store: %w", err)
}
// File doesn't exist, create it
if err := store.save(); err != nil {
return nil, fmt.Errorf("failed to create store file: %w", err)
}
}
return store, nil
}
// Create adds a new task
func (s *JSONStore) Create(task *Task) error {
s.mu.Lock()
defer s.mu.Unlock()
// Check for duplicate ID
for _, t := range s.data.Tasks {
if t.ID == task.ID {
return fmt.Errorf("task with ID %s already exists", task.ID)
}
}
s.data.Tasks = append(s.data.Tasks, task)
return s.save()
}
// Get retrieves a task by ID
func (s *JSONStore) Get(id string) (*Task, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for _, task := range s.data.Tasks {
if task.ID == id {
return task, nil
}
}
return nil, fmt.Errorf("task not found: %s", id)
}
// GetAll retrieves all tasks
func (s *JSONStore) GetAll() ([]*Task, error) {
s.mu.RLock()
defer s.mu.RUnlock()
// Return a copy to prevent external modification
tasks := make([]*Task, len(s.data.Tasks))
copy(tasks, s.data.Tasks)
return tasks, nil
}
// GetDue retrieves tasks that are due for execution
func (s *JSONStore) GetDue() ([]*Task, error) {
s.mu.RLock()
defer s.mu.RUnlock()
now := time.Now()
dueTasks := make([]*Task, 0)
for _, task := range s.data.Tasks {
if task.Status == TaskStatusActive && now.After(task.NextRun) {
dueTasks = append(dueTasks, task)
}
}
return dueTasks, nil
}
// GetByAgent retrieves all tasks for a specific agent
func (s *JSONStore) GetByAgent(agentName string) ([]*Task, error) {
s.mu.RLock()
defer s.mu.RUnlock()
agentTasks := make([]*Task, 0)
for _, task := range s.data.Tasks {
if task.AgentName == agentName {
agentTasks = append(agentTasks, task)
}
}
return agentTasks, nil
}
// Update updates an existing task
func (s *JSONStore) Update(task *Task) error {
s.mu.Lock()
defer s.mu.Unlock()
for i, t := range s.data.Tasks {
if t.ID == task.ID {
task.UpdatedAt = time.Now()
s.data.Tasks[i] = task
return s.save()
}
}
return fmt.Errorf("task not found: %s", task.ID)
}
// Delete removes a task
func (s *JSONStore) Delete(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
for i, task := range s.data.Tasks {
if task.ID == id {
// Remove task from slice
s.data.Tasks = append(s.data.Tasks[:i], s.data.Tasks[i+1:]...)
return s.save()
}
}
return fmt.Errorf("task not found: %s", id)
}
// LogRun records a task execution
func (s *JSONStore) LogRun(run *TaskRun) error {
s.mu.Lock()
defer s.mu.Unlock()
s.data.TaskRuns = append(s.data.TaskRuns, run)
return s.save()
}
// GetRuns retrieves execution history for a task
func (s *JSONStore) GetRuns(taskID string, limit int) ([]*TaskRun, error) {
s.mu.RLock()
defer s.mu.RUnlock()
runs := make([]*TaskRun, 0)
for i := len(s.data.TaskRuns) - 1; i >= 0 && len(runs) < limit; i-- {
if s.data.TaskRuns[i].TaskID == taskID {
runs = append(runs, s.data.TaskRuns[i])
}
}
return runs, nil
}
// Close releases resources (no-op for JSON store)
func (s *JSONStore) Close() error {
return nil
}
// load reads data from the JSON file
func (s *JSONStore) load() error {
file, err := os.ReadFile(s.filePath)
if err != nil {
return err
}
// Handle empty file
if len(file) == 0 {
return nil
}
if err := json.Unmarshal(file, s.data); err != nil {
return err
}
// Ensure slices are not nil after unmarshaling
if s.data.Tasks == nil {
s.data.Tasks = make([]*Task, 0)
}
if s.data.TaskRuns == nil {
s.data.TaskRuns = make([]*TaskRun, 0)
}
return nil
}
// save writes data to the JSON file
func (s *JSONStore) save() error {
data, err := json.MarshalIndent(s.data, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal data: %w", err)
}
basePath := filepath.Dir(s.filePath)
os.MkdirAll(basePath, 0755)
return os.WriteFile(s.filePath, data, 0644)
}
+249
View File
@@ -0,0 +1,249 @@
package scheduler
import (
"context"
"fmt"
"sync"
"time"
"github.com/mudler/xlog"
)
// Scheduler manages scheduled tasks
type Scheduler struct {
store TaskStore
executor AgentExecutor
pollInterval time.Duration
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
mu sync.RWMutex
runningTasks map[string]context.CancelFunc
}
// NewScheduler creates a new scheduler with the given store and executor
func NewScheduler(store TaskStore, executor AgentExecutor, pollInterval time.Duration) *Scheduler {
return &Scheduler{
store: store,
executor: executor,
pollInterval: pollInterval,
runningTasks: make(map[string]context.CancelFunc),
}
}
// Start begins the scheduler's polling loop
func (s *Scheduler) Start() {
if s.ctx != nil {
xlog.Warn("Scheduler already started")
return
}
ctx, cancel := context.WithCancel(context.Background())
s.ctx = ctx
s.cancel = cancel
s.wg.Add(1)
go s.run()
xlog.Info("Task scheduler started", "poll_interval", s.pollInterval)
}
// Stop gracefully stops the scheduler
func (s *Scheduler) Stop() {
if s.cancel != nil {
s.cancel()
}
s.wg.Wait()
s.store.Close()
xlog.Info("Task scheduler stopped")
s.cancel = nil
s.ctx = nil
}
// run is the main polling loop
func (s *Scheduler) run() {
defer s.wg.Done()
ticker := time.NewTicker(s.pollInterval)
defer ticker.Stop()
for {
select {
case <-s.ctx.Done():
return
case <-ticker.C:
s.processDueTasks()
}
}
}
// processDueTasks checks for and executes due tasks
func (s *Scheduler) processDueTasks() {
tasks, err := s.store.GetDue()
if err != nil {
xlog.Error("Failed to get due tasks", "error", err)
return
}
if len(tasks) > 0 {
xlog.Debug("Processing due tasks", "count", len(tasks))
}
for _, task := range tasks {
// Check if task is already running
s.mu.RLock()
_, running := s.runningTasks[task.ID]
s.mu.RUnlock()
if running {
xlog.Warn("Task already running, skipping", "task_id", task.ID)
continue
}
// Execute task in goroutine
s.wg.Add(1)
go s.executeTask(task)
}
}
// executeTask runs a single task
func (s *Scheduler) executeTask(task *Task) {
defer s.wg.Done()
taskCtx, cancel := context.WithCancel(s.ctx)
defer cancel()
// Register running task
s.mu.Lock()
s.runningTasks[task.ID] = cancel
s.mu.Unlock()
defer func() {
s.mu.Lock()
delete(s.runningTasks, task.ID)
s.mu.Unlock()
}()
xlog.Info("Executing task", "task_id", task.ID, "agent", task.AgentName, "prompt", task.Prompt)
startTime := time.Now()
run := NewTaskRun(task.ID)
// Execute the task
result, err := s.executor.Execute(taskCtx, task.AgentName, task.Prompt)
run.DurationMs = time.Since(startTime).Milliseconds()
if err != nil {
run.Status = "error"
run.Error = err.Error()
xlog.Error("Task execution failed", "task_id", task.ID, "error", err)
} else {
run.Status = "success"
if result != nil {
run.Result = result.Response
}
xlog.Info("Task executed successfully", "task_id", task.ID, "duration_ms", run.DurationMs)
}
// Log the run
if err := s.store.LogRun(run); err != nil {
xlog.Error("Failed to log task run", "task_id", task.ID, "error", err)
}
// Update task for next run
now := time.Now()
task.LastRun = &now
// For one-time tasks, mark as deleted
if task.ScheduleType == ScheduleTypeOnce {
if err := s.store.Delete(task.ID); err != nil {
xlog.Error("Failed to delete task", "task_id", task.ID, "error", err)
}
} else {
// Calculate next run
if err := task.CalculateNextRun(); err != nil {
xlog.Error("Failed to calculate next run", "task_id", task.ID, "error", err)
task.Status = TaskStatusPaused
}
}
if err := s.store.Update(task); err != nil {
xlog.Error("Failed to update task", "task_id", task.ID, "error", err)
}
}
// CRUD operations
// CreateTask adds a new task
func (s *Scheduler) CreateTask(task *Task) error {
return s.store.Create(task)
}
// GetTask retrieves a task by ID
func (s *Scheduler) GetTask(id string) (*Task, error) {
return s.store.Get(id)
}
// GetAllTasks retrieves all tasks
func (s *Scheduler) GetAllTasks() ([]*Task, error) {
return s.store.GetAll()
}
// GetTasksByAgent retrieves all tasks for a specific agent
func (s *Scheduler) GetTasksByAgent(agentName string) ([]*Task, error) {
return s.store.GetByAgent(agentName)
}
// UpdateTask updates an existing task
func (s *Scheduler) UpdateTask(task *Task) error {
return s.store.Update(task)
}
// DeleteTask removes a task
func (s *Scheduler) DeleteTask(id string) error {
return s.store.Delete(id)
}
// GetTaskRuns retrieves execution history for a task
func (s *Scheduler) GetTaskRuns(taskID string, limit int) ([]*TaskRun, error) {
return s.store.GetRuns(taskID, limit)
}
// PauseTask pauses a task
func (s *Scheduler) PauseTask(id string) error {
task, err := s.store.Get(id)
if err != nil {
return err
}
task.Status = TaskStatusPaused
return s.store.Update(task)
}
// ResumeTask resumes a paused task
func (s *Scheduler) ResumeTask(id string) error {
task, err := s.store.Get(id)
if err != nil {
return err
}
task.Status = TaskStatusActive
if err := task.CalculateNextRun(); err != nil {
return err
}
return s.store.Update(task)
}
// CancelRunningTask cancels a currently running task
func (s *Scheduler) CancelRunningTask(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
cancel, exists := s.runningTasks[id]
if !exists {
return fmt.Errorf("task not running: %s", id)
}
cancel()
return nil
}
+13
View File
@@ -0,0 +1,13 @@
package scheduler_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestScheduler(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Scheduler Suite")
}
+395
View File
@@ -0,0 +1,395 @@
package scheduler_test
import (
"context"
"errors"
"os"
"time"
"github.com/mudler/LocalAGI/core/scheduler"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
// MockExecutor for testing
type MockExecutor struct {
executedTasks []string
shouldError bool
}
func (m *MockExecutor) Execute(ctx context.Context, agentName string, prompt string) (*scheduler.JobResult, error) {
m.executedTasks = append(m.executedTasks, agentName+":"+prompt)
if m.shouldError {
return nil, errors.New("mock execution error")
}
return &scheduler.JobResult{Response: "test response"}, nil
}
var _ = Describe("Scheduler", func() {
var (
tempFile string
store scheduler.TaskStore
executor *MockExecutor
sched *scheduler.Scheduler
)
BeforeEach(func() {
// Create temporary file for JSON store
f, err := os.CreateTemp("", "scheduler_test_*.json")
Expect(err).NotTo(HaveOccurred())
tempFile = f.Name()
f.Close()
store, err = scheduler.NewJSONStore(tempFile)
Expect(err).NotTo(HaveOccurred())
executor = &MockExecutor{}
sched = scheduler.NewScheduler(store, executor, 100*time.Millisecond)
sched.Start()
})
AfterEach(func() {
if sched != nil {
sched.Stop()
}
os.Remove(tempFile)
})
Describe("Task Creation", func() {
It("should create a valid task with cron schedule", func() {
task, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "0 0 * * *")
Expect(err).NotTo(HaveOccurred())
Expect(task.ID).NotTo(BeEmpty())
Expect(task.AgentName).To(Equal("test-agent"))
Expect(task.Prompt).To(Equal("test prompt"))
Expect(task.ScheduleType).To(Equal(scheduler.ScheduleTypeCron))
Expect(task.Status).To(Equal(scheduler.TaskStatusActive))
Expect(task.NextRun).NotTo(BeZero())
})
It("should return error for invalid cron expression", func() {
_, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "invalid cron")
Expect(err).To(HaveOccurred())
})
It("should create a valid task with interval schedule", func() {
task, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeInterval, "3600000")
Expect(err).NotTo(HaveOccurred())
Expect(task.NextRun).To(BeTemporally("~", time.Now().Add(time.Hour), 5*time.Second))
})
It("should create a valid task with once schedule using duration", func() {
task, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeOnce, "24h")
Expect(err).NotTo(HaveOccurred())
Expect(task.NextRun).To(BeTemporally("~", time.Now().Add(24*time.Hour), 5*time.Second))
})
It("should create a valid task with once schedule using day syntax", func() {
task, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeOnce, "1d")
Expect(err).NotTo(HaveOccurred())
Expect(task.NextRun).To(BeTemporally("~", time.Now().Add(24*time.Hour), 5*time.Second))
})
It("should create a valid task with once schedule using combined day+time", func() {
task, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeOnce, "2d12h30m")
Expect(err).NotTo(HaveOccurred())
Expect(task.NextRun).To(BeTemporally("~", time.Now().Add(2*24*time.Hour+12*time.Hour+30*time.Minute), 5*time.Second))
})
It("should return error for invalid once duration", func() {
_, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeOnce, "invalid")
Expect(err).To(HaveOccurred())
})
})
Describe("Task IsDue", func() {
It("should return true for active task past due time", func() {
task := &scheduler.Task{
Status: scheduler.TaskStatusActive,
NextRun: time.Now().Add(-1 * time.Hour),
}
Expect(task.IsDue()).To(BeTrue())
})
It("should return false for active task not yet due", func() {
task := &scheduler.Task{
Status: scheduler.TaskStatusActive,
NextRun: time.Now().Add(1 * time.Hour),
}
Expect(task.IsDue()).To(BeFalse())
})
It("should return false for paused task even if past due", func() {
task := &scheduler.Task{
Status: scheduler.TaskStatusPaused,
NextRun: time.Now().Add(-1 * time.Hour),
}
Expect(task.IsDue()).To(BeFalse())
})
})
Describe("JSON Store", func() {
Context("CRUD operations", func() {
It("should create and retrieve a task", func() {
task, err := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "0 0 * * *")
Expect(err).NotTo(HaveOccurred())
err = store.Create(task)
Expect(err).NotTo(HaveOccurred())
retrieved, err := store.Get(task.ID)
Expect(err).NotTo(HaveOccurred())
Expect(retrieved.ID).To(Equal(task.ID))
Expect(retrieved.AgentName).To(Equal(task.AgentName))
Expect(retrieved.Prompt).To(Equal(task.Prompt))
})
It("should update a task", func() {
task, _ := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "0 0 * * *")
store.Create(task)
task.Prompt = "updated prompt"
err := store.Update(task)
Expect(err).NotTo(HaveOccurred())
updated, err := store.Get(task.ID)
Expect(err).NotTo(HaveOccurred())
Expect(updated.Prompt).To(Equal("updated prompt"))
})
It("should delete a task", func() {
task, _ := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "0 0 * * *")
store.Create(task)
err := store.Delete(task.ID)
Expect(err).NotTo(HaveOccurred())
_, err = store.Get(task.ID)
Expect(err).To(HaveOccurred())
})
It("should return error when getting non-existent task", func() {
_, err := store.Get("non-existent-id")
Expect(err).To(HaveOccurred())
})
})
Context("Querying tasks", func() {
BeforeEach(func() {
// Create test tasks
// task1: once schedule with 0s delay => immediately due
task1, err := scheduler.NewTask("agent1", "prompt1", scheduler.ScheduleTypeOnce, "0s")
Expect(err).NotTo(HaveOccurred())
task1.NextRun = time.Now().Add(-1 * time.Hour) // force into the past
// task2: cron schedule => next run in the future, not due
task2, err := scheduler.NewTask("agent2", "prompt2", scheduler.ScheduleTypeCron, "0 0 1 1 *")
Expect(err).NotTo(HaveOccurred())
// task3: once schedule but paused => not due
task3, err := scheduler.NewTask("agent1", "prompt3", scheduler.ScheduleTypeOnce, "0s")
Expect(err).NotTo(HaveOccurred())
task3.Status = scheduler.TaskStatusPaused
Expect(store.Create(task1)).To(Succeed())
Expect(store.Create(task2)).To(Succeed())
Expect(store.Create(task3)).To(Succeed())
})
It("should get all tasks", func() {
tasks, err := store.GetAll()
Expect(err).NotTo(HaveOccurred())
Expect(tasks).To(HaveLen(3))
})
It("should get only due tasks", func() {
dueTasks, err := store.GetDue()
Expect(err).NotTo(HaveOccurred())
Expect(dueTasks).To(HaveLen(1))
Expect(dueTasks[0].AgentName).To(Equal("agent1"))
Expect(dueTasks[0].Prompt).To(Equal("prompt1"))
})
It("should get tasks by agent", func() {
agentTasks, err := store.GetByAgent("agent1")
Expect(err).NotTo(HaveOccurred())
Expect(agentTasks).To(HaveLen(2))
})
})
Context("Task runs", func() {
It("should log and retrieve task runs", func() {
task, _ := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "0 0 * * *")
store.Create(task)
run := scheduler.NewTaskRun(task.ID)
run.Status = "success"
run.Result = "test result"
run.DurationMs = 1000
err := store.LogRun(run)
Expect(err).NotTo(HaveOccurred())
runs, err := store.GetRuns(task.ID, 10)
Expect(err).NotTo(HaveOccurred())
Expect(runs).To(HaveLen(1))
Expect(runs[0].Status).To(Equal("success"))
Expect(runs[0].Result).To(Equal("test result"))
})
It("should limit returned runs", func() {
task, _ := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "0 0 * * *")
store.Create(task)
// Create 5 runs
for i := 0; i < 5; i++ {
run := scheduler.NewTaskRun(task.ID)
store.LogRun(run)
}
runs, err := store.GetRuns(task.ID, 3)
Expect(err).NotTo(HaveOccurred())
Expect(runs).To(HaveLen(3))
})
})
Context("Persistence", func() {
It("should persist data across store instances", func() {
task, _ := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeCron, "0 0 * * *")
store.Create(task)
store.Close()
// Create new store instance with same file
newStore, err := scheduler.NewJSONStore(tempFile)
Expect(err).NotTo(HaveOccurred())
defer newStore.Close()
retrieved, err := newStore.Get(task.ID)
Expect(err).NotTo(HaveOccurred())
Expect(retrieved.ID).To(Equal(task.ID))
})
})
})
Describe("Scheduler Execution", func() {
It("should execute a due task", func() {
task, _ := scheduler.NewTask("test-agent", "test prompt", scheduler.ScheduleTypeOnce, "0s")
task.NextRun = time.Now().Add(-1 * time.Second) // force into the past
err := sched.CreateTask(task)
Expect(err).NotTo(HaveOccurred())
// Scheduler is already started in BeforeEach
Eventually(func() int {
return len(executor.executedTasks)
}, "2s", "100ms").Should(Equal(1))
Expect(executor.executedTasks[0]).To(Equal("test-agent:test prompt"))
// Verify task run was logged
runs, err := sched.GetTaskRuns(task.ID, 10)
Expect(err).NotTo(HaveOccurred())
Expect(runs).To(HaveLen(1))
Expect(runs[0].Status).To(Equal("success"))
// Verify one-time task was deleted
_, err = sched.GetTask(task.ID)
Expect(err).To(HaveOccurred())
})
It("should execute recurring tasks multiple times", func() {
task, _ := scheduler.NewTask("test-agent", "recurring", scheduler.ScheduleTypeInterval, "500")
task.NextRun = time.Now().Add(-1 * time.Second)
err := sched.CreateTask(task)
Expect(err).NotTo(HaveOccurred())
// Scheduler is already started in BeforeEach
Eventually(func() int {
return len(executor.executedTasks)
}, "3s", "100ms").Should(BeNumerically(">=", 2))
// Verify task is still active
updatedTask, err := sched.GetTask(task.ID)
Expect(err).NotTo(HaveOccurred())
Expect(updatedTask.Status).To(Equal(scheduler.TaskStatusActive))
})
It("should handle task execution errors", func() {
executor.shouldError = true
task, _ := scheduler.NewTask("test-agent", "error task", scheduler.ScheduleTypeOnce, "0s")
task.NextRun = time.Now().Add(-1 * time.Second) // force into the past
sched.CreateTask(task)
// Scheduler is already started in BeforeEach
Eventually(func() int {
runs, _ := sched.GetTaskRuns(task.ID, 10)
return len(runs)
}, "2s", "100ms").Should(Equal(1))
runs, _ := sched.GetTaskRuns(task.ID, 10)
Expect(runs[0].Status).To(Equal("error"))
Expect(runs[0].Error).NotTo(BeEmpty())
})
It("should not execute paused tasks", func() {
task, _ := scheduler.NewTask("test-agent", "paused", scheduler.ScheduleTypeOnce, "0s")
task.NextRun = time.Now().Add(-1 * time.Second) // force into the past
task.Status = scheduler.TaskStatusPaused
sched.CreateTask(task)
// Scheduler is already started in BeforeEach
Consistently(func() int {
return len(executor.executedTasks)
}, "1s", "100ms").Should(Equal(0))
})
})
Describe("Task Management", func() {
It("should pause and resume a task", func() {
task, _ := scheduler.NewTask("test-agent", "test", scheduler.ScheduleTypeCron, "0 0 * * *")
sched.CreateTask(task)
err := sched.PauseTask(task.ID)
Expect(err).NotTo(HaveOccurred())
paused, _ := sched.GetTask(task.ID)
Expect(paused.Status).To(Equal(scheduler.TaskStatusPaused))
err = sched.ResumeTask(task.ID)
Expect(err).NotTo(HaveOccurred())
resumed, _ := sched.GetTask(task.ID)
Expect(resumed.Status).To(Equal(scheduler.TaskStatusActive))
Expect(resumed.NextRun).NotTo(BeZero())
})
It("should get tasks by agent", func() {
task1, err := scheduler.NewTask("agent1", "prompt1", scheduler.ScheduleTypeCron, "0 0 * * *")
Expect(err).NotTo(HaveOccurred())
task2, err := scheduler.NewTask("agent2", "prompt2", scheduler.ScheduleTypeCron, "0 0 * * *")
Expect(err).NotTo(HaveOccurred())
task3, err := scheduler.NewTask("agent1", "prompt3", scheduler.ScheduleTypeCron, "0 0 * * *")
Expect(err).NotTo(HaveOccurred())
Expect(sched.CreateTask(task1)).To(Succeed())
Expect(sched.CreateTask(task2)).To(Succeed())
Expect(sched.CreateTask(task3)).To(Succeed())
agent1Tasks, err := sched.GetTasksByAgent("agent1")
Expect(err).NotTo(HaveOccurred())
Expect(agent1Tasks).To(HaveLen(2))
})
It("should delete a task", func() {
task, _ := scheduler.NewTask("test-agent", "test", scheduler.ScheduleTypeCron, "0 0 * * *")
sched.CreateTask(task)
err := sched.DeleteTask(task.ID)
Expect(err).NotTo(HaveOccurred())
_, err = sched.GetTask(task.ID)
Expect(err).To(HaveOccurred())
})
})
})
+156
View File
@@ -0,0 +1,156 @@
package scheduler
import (
"fmt"
"regexp"
"strconv"
"time"
"github.com/google/uuid"
"github.com/robfig/cron/v3"
)
var dayPattern = regexp.MustCompile(`^(\d+)d(.*)$`)
// ParseDuration extends time.ParseDuration with support for days ("d").
// Examples: "1d" = 24h, "2d12h" = 60h, "30m", "2h30m".
func ParseDuration(s string) (time.Duration, error) {
if m := dayPattern.FindStringSubmatch(s); m != nil {
days, err := strconv.Atoi(m[1])
if err != nil {
return 0, fmt.Errorf("invalid duration: %s", s)
}
d := time.Duration(days) * 24 * time.Hour
if m[2] != "" {
rest, err := time.ParseDuration(m[2])
if err != nil {
return 0, fmt.Errorf("invalid duration: %w", err)
}
d += rest
}
return d, nil
}
return time.ParseDuration(s)
}
type TaskStatus string
const (
TaskStatusActive TaskStatus = "active"
TaskStatusPaused TaskStatus = "paused"
)
type ScheduleType string
const (
ScheduleTypeCron ScheduleType = "cron"
ScheduleTypeInterval ScheduleType = "interval"
ScheduleTypeOnce ScheduleType = "once"
)
// Task represents a scheduled task
type Task struct {
ID string `json:"id"`
AgentName string `json:"agent_name"`
Prompt string `json:"prompt"`
ScheduleType ScheduleType `json:"schedule_type"`
ScheduleValue string `json:"schedule_value"`
Status TaskStatus `json:"status"`
NextRun time.Time `json:"next_run"`
LastRun *time.Time `json:"last_run,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ContextMode string `json:"context_mode"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// TaskRun represents a single execution of a task
type TaskRun struct {
ID string `json:"id"`
TaskID string `json:"task_id"`
RunAt time.Time `json:"run_at"`
DurationMs int64 `json:"duration_ms"`
Status string `json:"status"` // "success", "error", "timeout"
Result string `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
// NewTask creates a new task with the given parameters
func NewTask(agentName, prompt string, scheduleType ScheduleType, scheduleValue string) (*Task, error) {
task := &Task{
ID: uuid.New().String(),
AgentName: agentName,
Prompt: prompt,
ScheduleType: scheduleType,
ScheduleValue: scheduleValue,
Status: TaskStatusActive,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
ContextMode: "agent",
Metadata: make(map[string]interface{}),
}
if err := task.CalculateNextRun(); err != nil {
return nil, err
}
return task, nil
}
// CalculateNextRun calculates the next run time based on schedule type
func (t *Task) CalculateNextRun() error {
now := time.Now()
switch t.ScheduleType {
case ScheduleTypeCron:
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
schedule, err := parser.Parse(t.ScheduleValue)
if err != nil {
return fmt.Errorf("invalid cron expression: %w", err)
}
t.NextRun = schedule.Next(now)
case ScheduleTypeInterval:
intervalMs, err := strconv.ParseInt(t.ScheduleValue, 10, 64)
if err != nil {
return fmt.Errorf("invalid interval: %w", err)
}
if intervalMs <= 0 {
return fmt.Errorf("invalid interval: %d", intervalMs)
}
if t.LastRun != nil {
t.NextRun = t.LastRun.Add(time.Duration(intervalMs) * time.Millisecond)
} else {
t.NextRun = now.Add(time.Duration(intervalMs) * time.Millisecond)
}
case ScheduleTypeOnce:
duration, err := ParseDuration(t.ScheduleValue)
if err != nil {
return fmt.Errorf("invalid duration: %w", err)
}
if duration < 0 {
return fmt.Errorf("duration must be positive: %s", t.ScheduleValue)
}
t.NextRun = now.Add(duration)
default:
return fmt.Errorf("unknown schedule type: %s", t.ScheduleType)
}
return nil
}
// IsDue checks if the task should be executed now
func (t *Task) IsDue() bool {
return t.Status == TaskStatusActive && time.Now().After(t.NextRun)
}
// NewTaskRun creates a new task run record
func NewTaskRun(taskID string) *TaskRun {
return &TaskRun{
ID: uuid.New().String(),
TaskID: taskID,
RunAt: time.Now(),
}
}
+13
View File
@@ -27,6 +27,8 @@ type (
Manager interface {
Send(message Envelope)
Handle(ctx *fiber.Ctx, cl Listener)
Register(cl Listener)
Unregister(id string)
Clients() []string
}
@@ -110,6 +112,17 @@ func (manager *broadcastManager) Send(message Envelope) {
manager.broadcast <- message
}
// Register adds a client to the broadcast list and sends message history.
func (manager *broadcastManager) Register(cl Listener) {
manager.register(cl)
manager.messageHistory.Send(cl)
}
// Unregister removes a client from the broadcast list and closes its channel.
func (manager *broadcastManager) Unregister(id string) {
manager.unregister(id)
}
// Handle sets up a new client and handles the connection.
func (manager *broadcastManager) Handle(c *fiber.Ctx, cl Listener) {
+241
View File
@@ -0,0 +1,241 @@
package state
import (
"context"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/mudler/LocalAGI/pkg/llm"
"github.com/mudler/LocalAGI/pkg/localrag"
"github.com/mudler/xlog"
"github.com/sashabaranov/go-openai"
)
// KBCompactionClient is the interface used by compaction. It can be implemented by the HTTP RAG client adapter or by the in-process collection adapter.
type KBCompactionClient interface {
Collection() string
ListEntries() ([]string, error)
GetEntryContent(entry string) (content string, chunkCount int, err error)
Store(filePath string) error
DeleteEntry(entry string) error
}
// wrappedClientCompactionAdapter adapts *localrag.WrappedClient to KBCompactionClient.
type wrappedClientCompactionAdapter struct {
*localrag.WrappedClient
}
func (a *wrappedClientCompactionAdapter) ListEntries() ([]string, error) {
return a.Client.ListEntries(a.Collection())
}
func (a *wrappedClientCompactionAdapter) Store(filePath string) error {
_, err := a.Client.Store(a.Collection(), filePath)
return err
}
func (a *wrappedClientCompactionAdapter) DeleteEntry(entry string) error {
_, err := a.Client.DeleteEntry(a.Collection(), entry)
return err
}
// datePrefixRegex matches YYYY-MM-DD at the start of a filename (e.g. 2006-01-02-15-04-05-hash.txt).
var datePrefixRegex = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2})`)
// summaryPrefix is the filename prefix for compaction summary entries; skip re-compacting these.
const summaryPrefix = "summary-"
// bucketKey returns the period bucket key for a date string (YYYY-MM-DD).
func bucketKey(dateStr, period string) (string, error) {
t, err := time.Parse("2006-01-02", dateStr)
if err != nil {
return "", err
}
switch period {
case "daily":
return dateStr, nil
case "weekly":
year, week := t.ISOWeek()
return fmt.Sprintf("%04d-W%02d", year, week), nil
case "monthly":
return t.Format("2006-01"), nil
default:
return dateStr, nil
}
}
// dateFromFilename extracts YYYY-MM-DD from the start of a filename if present.
func dateFromFilename(filename string) (string, bool) {
base := filepath.Base(filename)
matches := datePrefixRegex.FindStringSubmatch(base)
if len(matches) < 2 {
return "", false
}
return matches[1], true
}
// groupEntriesByPeriod groups entry names by period bucket (daily/weekly/monthly). Skips summary-* and entries without a parseable date.
func groupEntriesByPeriod(entries []string, period string) map[string][]string {
groups := make(map[string][]string)
for _, entry := range entries {
if strings.HasPrefix(filepath.Base(entry), summaryPrefix) {
continue
}
dateStr, ok := dateFromFilename(entry)
if !ok {
continue
}
key, err := bucketKey(dateStr, period)
if err != nil {
xlog.Debug("compaction: skip entry, invalid date", "entry", entry, "error", err)
continue
}
groups[key] = append(groups[key], entry)
}
return groups
}
// summarizer summarizes text via the LLM.
type summarizer interface {
Summarize(ctx context.Context, content string) (string, error)
}
type openAISummarizer struct {
client *openai.Client
model string
}
func (s *openAISummarizer) Summarize(ctx context.Context, content string) (string, error) {
if content == "" {
return "", nil
}
resp, err := s.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
Model: s.model,
Messages: []openai.ChatCompletionMessage{
{Role: openai.ChatMessageRoleSystem, Content: "Summarize the following knowledge base entries into a concise summary. Preserve important facts and key points."},
{Role: openai.ChatMessageRoleUser, Content: content},
},
})
if err != nil {
return "", err
}
if len(resp.Choices) == 0 {
return "", fmt.Errorf("no completion choices")
}
return strings.TrimSpace(resp.Choices[0].Message.Content), nil
}
// RunCompaction runs one compaction pass: list entries, group by period, for each group fetch content, optionally summarize, store result, delete originals.
func RunCompaction(ctx context.Context, client KBCompactionClient, period string, summarize bool, apiURL, apiKey, model string) error {
collection := client.Collection()
entries, err := client.ListEntries()
if err != nil {
return fmt.Errorf("list entries: %w", err)
}
groups := groupEntriesByPeriod(entries, period)
if len(groups) == 0 {
xlog.Debug("compaction: no groups to compact", "collection", collection, "period", period)
return nil
}
var sum summarizer
if summarize && apiURL != "" && model != "" {
openAIClient := llm.NewClient(apiKey, apiURL, "120s")
sum = &openAISummarizer{client: openAIClient, model: model}
}
for key, groupEntries := range groups {
if len(groupEntries) == 0 {
continue
}
var combined strings.Builder
for _, entry := range groupEntries {
entryContent, _, err := client.GetEntryContent(entry)
if err != nil {
xlog.Warn("compaction: get entry content failed", "entry", entry, "error", err)
continue
}
if entryContent != "" {
combined.WriteString(entryContent)
combined.WriteString("\n\n")
}
}
content := strings.TrimSpace(combined.String())
if content == "" {
xlog.Debug("compaction: empty content for group", "key", key)
continue
}
if sum != nil {
summary, err := sum.Summarize(ctx, content)
if err != nil {
xlog.Warn("compaction: summarize failed", "key", key, "error", err)
continue
}
content = summary
}
// Store result as summary-<key>.txt
resultFilename := fmt.Sprintf("%s%s.txt", summaryPrefix, key)
tmpDir, err := os.MkdirTemp("", "localagi-compact")
if err != nil {
xlog.Warn("compaction: mkdir temp failed", "error", err)
continue
}
tmpPath := filepath.Join(tmpDir, resultFilename)
if err := os.WriteFile(tmpPath, []byte(content), 0644); err != nil {
os.RemoveAll(tmpDir)
xlog.Warn("compaction: write temp file failed", "error", err)
continue
}
if err := client.Store(tmpPath); err != nil {
os.RemoveAll(tmpDir)
xlog.Warn("compaction: store failed", "key", key, "error", err)
continue
}
os.RemoveAll(tmpDir)
for _, entry := range groupEntries {
if err := client.DeleteEntry(entry); err != nil {
xlog.Warn("compaction: delete entry failed", "entry", entry, "error", err)
}
}
xlog.Info("compaction: compacted group", "collection", collection, "period", period, "key", key, "entries", len(groupEntries))
}
return nil
}
// runCompactionTicker runs compaction on a schedule (daily/weekly/monthly). It stops when ctx is done.
func runCompactionTicker(ctx context.Context, client KBCompactionClient, config *AgentConfig, apiURL, apiKey, model string) {
// Run first compaction immediately on startup
if err := RunCompaction(ctx, client, config.KBCompactionInterval, config.KBCompactionSummarize, apiURL, apiKey, model); err != nil {
xlog.Warn("compaction ticker initial run failed", "collection", client.Collection(), "error", err)
}
interval := 24 * time.Hour
switch config.KBCompactionInterval {
case "weekly":
interval = 7 * 24 * time.Hour
case "monthly":
interval = 30 * 24 * time.Hour
default:
interval = 24 * time.Hour
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
xlog.Debug("compaction ticker stopped", "collection", client.Collection())
return
case <-ticker.C:
if err := RunCompaction(ctx, client, config.KBCompactionInterval, config.KBCompactionSummarize, apiURL, apiKey, model); err != nil {
xlog.Warn("compaction ticker failed", "collection", client.Collection(), "error", err)
}
}
}
}
+492 -29
View File
@@ -2,12 +2,30 @@ package state
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
)
// parseIntField parses an integer field that may be received as either a number or a string
func parseIntField(value interface{}) int {
switch v := value.(type) {
case int:
return v
case float64:
return int(v)
case string:
if i, err := strconv.Atoi(v); err == nil {
return i
}
}
return 0
}
type ConnectorConfig struct {
Type string `json:"type"` // e.g. Slack
Config string `json:"config"`
@@ -29,41 +47,77 @@ func (d DynamicPromptsConfig) ToMap() map[string]string {
return config
}
type FiltersConfig struct {
Type string `json:"type"`
Config string `json:"config"`
}
type AgentConfig struct {
Connector []ConnectorConfig `json:"connectors" form:"connectors" `
Actions []ActionsConfig `json:"actions" form:"actions"`
DynamicPrompts []DynamicPromptsConfig `json:"dynamic_prompts" form:"dynamic_prompts"`
MCPServers []agent.MCPServer `json:"mcp_servers" form:"mcp_servers"`
Connector []ConnectorConfig `json:"connectors" form:"connectors" `
Actions []ActionsConfig `json:"actions" form:"actions"`
DynamicPrompts []DynamicPromptsConfig `json:"dynamic_prompts" form:"dynamic_prompts"`
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"`
Filters []FiltersConfig `json:"filters" form:"filters"`
Description string `json:"description" form:"description"`
Model string `json:"model" form:"model"`
MultimodalModel string `json:"multimodal_model" form:"multimodal_model"`
APIURL string `json:"api_url" form:"api_url"`
APIKey string `json:"api_key" form:"api_key"`
LocalRAGURL string `json:"local_rag_url" form:"local_rag_url"`
LocalRAGAPIKey string `json:"local_rag_api_key" form:"local_rag_api_key"`
Model string `json:"model" form:"model"`
MultimodalModel string `json:"multimodal_model" form:"multimodal_model"`
TranscriptionModel string `json:"transcription_model" form:"transcription_model"`
TranscriptionLanguage string `json:"transcription_language" form:"transcription_language"`
TTSModel string `json:"tts_model" form:"tts_model"`
APIURL string `json:"api_url" form:"api_url"`
APIKey string `json:"api_key" form:"api_key"`
LocalRAGURL string `json:"local_rag_url" form:"local_rag_url"`
LocalRAGAPIKey string `json:"local_rag_api_key" form:"local_rag_api_key"`
LastMessageDuration string `json:"last_message_duration" form:"last_message_duration"`
Name string `json:"name" form:"name"`
HUD bool `json:"hud" form:"hud"`
StandaloneJob bool `json:"standalone_job" form:"standalone_job"`
RandomIdentity bool `json:"random_identity" form:"random_identity"`
InitiateConversations bool `json:"initiate_conversations" form:"initiate_conversations"`
CanPlan bool `json:"enable_planning" form:"enable_planning"`
IdentityGuidance string `json:"identity_guidance" form:"identity_guidance"`
PeriodicRuns string `json:"periodic_runs" form:"periodic_runs"`
PermanentGoal string `json:"permanent_goal" form:"permanent_goal"`
EnableKnowledgeBase bool `json:"enable_kb" form:"enable_kb"`
EnableReasoning bool `json:"enable_reasoning" form:"enable_reasoning"`
KnowledgeBaseResults int `json:"kb_results" form:"kb_results"`
LoopDetectionSteps int `json:"loop_detection_steps" form:"loop_detection_steps"`
CanStopItself bool `json:"can_stop_itself" form:"can_stop_itself"`
SystemPrompt string `json:"system_prompt" form:"system_prompt"`
LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"`
SummaryLongTermMemory bool `json:"summary_long_term_memory" form:"summary_long_term_memory"`
Name string `json:"name" form:"name"`
HUD bool `json:"hud" form:"hud"`
StandaloneJob bool `json:"standalone_job" form:"standalone_job"`
RandomIdentity bool `json:"random_identity" form:"random_identity"`
InitiateConversations bool `json:"initiate_conversations" form:"initiate_conversations"`
CanPlan bool `json:"enable_planning" form:"enable_planning"`
PlanReviewerModel string `json:"plan_reviewer_model" form:"plan_reviewer_model"`
DisableSinkState bool `json:"disable_sink_state" form:"disable_sink_state"`
IdentityGuidance string `json:"identity_guidance" form:"identity_guidance"`
PeriodicRuns string `json:"periodic_runs" form:"periodic_runs"`
SchedulerPollInterval string `json:"scheduler_poll_interval" form:"scheduler_poll_interval"`
SchedulerTaskTemplate string `json:"scheduler_task_template" form:"scheduler_task_template"`
PermanentGoal string `json:"permanent_goal" form:"permanent_goal"`
EnableKnowledgeBase bool `json:"enable_kb" form:"enable_kb"`
EnableKBCompaction bool `json:"enable_kb_compaction" form:"enable_kb_compaction"`
KBCompactionInterval string `json:"kb_compaction_interval" form:"kb_compaction_interval"`
KBCompactionSummarize bool `json:"kb_compaction_summarize" form:"kb_compaction_summarize"`
KBAutoSearch bool `json:"kb_auto_search" form:"kb_auto_search"`
KBAsTools bool `json:"kb_as_tools" form:"kb_as_tools"`
EnableReasoning bool `json:"enable_reasoning" form:"enable_reasoning"`
EnableForceReasoningTool bool `json:"enable_reasoning_tool" form:"enable_reasoning_tool"`
EnableGuidedTools bool `json:"enable_guided_tools" form:"enable_guided_tools"`
EnableSkills bool `json:"enable_skills" form:"enable_skills"`
KnowledgeBaseResults int `json:"kb_results" form:"kb_results"`
CanStopItself bool `json:"can_stop_itself" form:"can_stop_itself"`
SystemPrompt string `json:"system_prompt" form:"system_prompt"`
SkillsPrompt string `json:"skills_prompt" form:"skills_prompt"`
InnerMonologueTemplate string `json:"inner_monologue_template" form:"inner_monologue_template"`
LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"`
SummaryLongTermMemory bool `json:"summary_long_term_memory" form:"summary_long_term_memory"`
ConversationStorageMode string `json:"conversation_storage_mode" form:"conversation_storage_mode"`
ParallelJobs int `json:"parallel_jobs" form:"parallel_jobs"`
CancelPreviousOnNewMessage *bool `json:"cancel_previous_on_new_message" form:"cancel_previous_on_new_message"`
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"`
MaxAttempts int `json:"max_attempts" form:"max_attempts"`
LoopDetection int `json:"loop_detection" form:"loop_detection"`
EnableAutoCompaction bool `json:"enable_auto_compaction" form:"enable_auto_compaction"`
AutoCompactionThreshold int `json:"auto_compaction_threshold" form:"auto_compaction_threshold"`
}
type AgentConfigMeta struct {
Filters []config.FieldGroup
Fields []config.Field
Connectors []config.FieldGroup
Actions []config.FieldGroup
@@ -75,6 +129,7 @@ func NewAgentConfigMeta(
actionsConfig []config.FieldGroup,
connectorsConfig []config.FieldGroup,
dynamicPromptsConfig []config.FieldGroup,
filtersConfig []config.FieldGroup,
) AgentConfigMeta {
return AgentConfigMeta{
Fields: []config.Field{
@@ -128,6 +183,34 @@ func NewAgentConfigMeta(
DefaultValue: "",
Tags: config.Tags{Section: "ModelSettings"},
},
{
Name: "transcription_model",
Label: "Transcription Model",
Type: "text",
DefaultValue: "",
Tags: config.Tags{Section: "ModelSettings"},
},
{
Name: "transcription_language",
Label: "Transcription Language",
Type: "text",
DefaultValue: "",
Tags: config.Tags{Section: "ModelSettings"},
},
{
Name: "tts_model",
Label: "TTS Model",
Type: "text",
DefaultValue: "",
Tags: config.Tags{Section: "ModelSettings"},
},
{
Name: "plan_reviewer_model",
Label: "Plan Reviewer Model",
Type: "text",
DefaultValue: "",
Tags: config.Tags{Section: "ModelSettings"},
},
{
Name: "api_url",
Label: "API URL",
@@ -172,6 +255,31 @@ func NewAgentConfigMeta(
Step: 1,
Tags: config.Tags{Section: "MemorySettings"},
},
{
Name: "enable_kb_compaction",
Label: "Enable KB Compaction",
Type: "checkbox",
DefaultValue: false,
HelpText: "Periodically group collection entries by date (daily/weekly/monthly), optionally summarize or concatenate, then store and remove originals",
Tags: config.Tags{Section: "MemorySettings"},
},
{
Name: "kb_compaction_interval",
Label: "KB Compaction Interval",
Type: "text",
DefaultValue: "daily",
Placeholder: "daily, weekly, monthly",
HelpText: "Compaction window: daily, weekly, or monthly",
Tags: config.Tags{Section: "MemorySettings"},
},
{
Name: "kb_compaction_summarize",
Label: "KB Compaction Summarize",
Type: "checkbox",
DefaultValue: true,
HelpText: "When enabled, summarize grouped content via LLM; when disabled, store concatenated content only (no LLM call)",
Tags: config.Tags{Section: "MemorySettings"},
},
{
Name: "long_term_memory",
Label: "Long Term Memory",
@@ -186,6 +294,35 @@ func NewAgentConfigMeta(
DefaultValue: false,
Tags: config.Tags{Section: "MemorySettings"},
},
{
Name: "kb_auto_search",
Label: "KB Auto Search",
Type: "checkbox",
DefaultValue: true,
HelpText: "Automatically search knowledge base when a user message is received",
Tags: config.Tags{Section: "MemorySettings"},
},
{
Name: "kb_as_tools",
Label: "KB As Tools",
Type: "checkbox",
DefaultValue: false,
HelpText: "Inject knowledge base search and add actions as tools, allowing the agent to access its memory without manual configuration",
Tags: config.Tags{Section: "MemorySettings"},
},
{
Name: "conversation_storage_mode",
Label: "Conversation Storage Mode",
Type: "select",
DefaultValue: "user_only",
Options: []config.FieldOption{
{Value: "user_only", Label: "User Messages Only"},
{Value: "user_and_assistant", Label: "User and Assistant Messages"},
{Value: "whole_conversation", Label: "Whole Conversation as Block"},
},
HelpText: "Controls what gets stored in the knowledge base: only user messages, user and assistant messages separately, or the entire conversation as a single block",
Tags: config.Tags{Section: "MemorySettings"},
},
{
Name: "system_prompt",
Label: "System Prompt",
@@ -202,6 +339,30 @@ func NewAgentConfigMeta(
HelpText: "Long-term objective for the agent to pursue",
Tags: config.Tags{Section: "PromptsGoals"},
},
{
Name: "skills_prompt",
Label: "Skills Prompt",
Type: "textarea",
DefaultValue: "",
HelpText: "Optional instructions for using skills. Used when Enable Skills is on. If empty, default instructions are used.",
Tags: config.Tags{Section: "PromptsGoals"},
},
{
Name: "inner_monologue_template",
Label: "Inner Monologue Template",
Type: "textarea",
DefaultValue: "",
HelpText: "Prompt used for periodic/standalone runs when the agent evaluates what to do next. If empty, the default autonomous agent instructions are used.",
Tags: config.Tags{Section: "PromptsGoals"},
},
{
Name: "scheduler_task_template",
Label: "Scheduler Task Template",
Type: "textarea",
DefaultValue: "",
HelpText: "Template for scheduled/recurring tasks. Use {{.Task}} to reference the task. Example: \"Execute: {{.Task}}\". If empty, the default inner monologue template is used with the task injected.",
Tags: config.Tags{Section: "PromptsGoals"},
},
{
Name: "standalone_job",
Label: "Standalone Job",
@@ -226,6 +387,24 @@ func NewAgentConfigMeta(
HelpText: "Enable agent to create and execute plans",
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
Name: "cancel_previous_on_new_message",
Label: "Cancel previous message on new message",
Type: "checkbox",
DefaultValue: true,
HelpText: "When a new message arrives for the same conversation, cancel the currently running job and start the new one. If disabled, new messages are queued.",
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
Name: "loop_detection",
Label: "Loop Detection",
Type: "number",
DefaultValue: 5,
Min: 1,
Step: 1,
HelpText: "Number of messages to check for loop detection. If a message is the same as the previous message, the job is cancelled.",
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
Name: "can_stop_itself",
Label: "Can Stop Itself",
@@ -243,6 +422,15 @@ func NewAgentConfigMeta(
HelpText: "Duration for scheduling periodic agent runs",
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
Name: "scheduler_poll_interval",
Label: "Scheduler Poll Interval",
Type: "text",
DefaultValue: "30s",
Placeholder: "30s",
HelpText: "Duration for polling the scheduler for planned tasks",
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
Name: "enable_reasoning",
Label: "Enable Reasoning",
@@ -252,12 +440,123 @@ func NewAgentConfigMeta(
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
Name: "loop_detection_steps",
Label: "Max Loop Detection Steps",
Name: "enable_reasoning_tool",
Label: "Enable Reasoning for tools",
Type: "checkbox",
DefaultValue: true,
HelpText: "Enable agent to reason more on tools",
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
Name: "enable_guided_tools",
Label: "Enable Guided Tools",
Type: "checkbox",
DefaultValue: false,
HelpText: "Filter tools through guidance using their descriptions; creates virtual guidelines when none exist",
Tags: config.Tags{Section: "AdvancedSettings"},
},
{
Name: "enable_skills",
Label: "Enable Skills",
Type: "checkbox",
DefaultValue: false,
HelpText: "Inject available skills into the agent and expose skill tools (list, read, search, resources) via MCP",
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: "disable_sink_state",
Label: "Disable Sink State",
Type: "checkbox",
DefaultValue: false,
HelpText: "Disable the sink state of the agent",
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: "MCP"},
},
{
Name: "mcp_prepare_script",
Label: "MCP Prepare Script",
Type: "textarea",
DefaultValue: "",
HelpText: "Script to prepare for running MCP servers",
Tags: config.Tags{Section: "MCP"},
},
{
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_auto_compaction",
Label: "Enable Auto Compaction",
Type: "checkbox",
DefaultValue: false,
HelpText: "Enable automatic conversation compaction when token threshold is reached",
Tags: config.Tags{Section: "ModelSettings"},
},
{
Name: "auto_compaction_threshold",
Label: "Auto Compaction Threshold (tokens)",
Type: "number",
DefaultValue: 4096,
Min: 1,
Step: 1,
HelpText: "Number of tokens to trigger automatic compaction",
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: "max_attempts",
Label: "Max Attempts",
Type: "number",
DefaultValue: 1,
Min: 1,
Step: 1,
HelpText: "Number of attempts on failure before surfacing the error to the user (1 = no retries)",
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"},
},
},
@@ -278,6 +577,7 @@ func NewAgentConfigMeta(
DynamicPrompts: dynamicPromptsConfig,
Connectors: connectorsConfig,
Actions: actionsConfig,
Filters: filtersConfig,
}
}
@@ -286,3 +586,166 @@ type Connector interface {
AgentReasoningCallback() func(state types.ActionCurrentState) bool
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"`
MaxEvaluationLoops interface{} `json:"max_evaluation_loops"`
MaxAttempts interface{} `json:"max_attempts"`
ParallelJobs interface{} `json:"parallel_jobs"`
KnowledgeBaseResults interface{} `json:"kb_results"`
}{
Alias: (*Alias)(a),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
// Parse integer fields that may come as strings
a.MaxEvaluationLoops = parseIntField(aux.MaxEvaluationLoops)
a.MaxAttempts = parseIntField(aux.MaxAttempts)
a.ParallelJobs = parseIntField(aux.ParallelJobs)
a.KnowledgeBaseResults = parseIntField(aux.KnowledgeBaseResults)
a.LoopDetection = parseIntField(aux.LoopDetection)
// 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 name, 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{
Name: name,
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")
}
name, _ := serverMap["name"].(string)
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{
Name: name,
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]
}
}
key := server.Name
if key == "" {
key = fmt.Sprintf("server%d", i)
}
mcpConfig.MCPServers[key] = 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)
}
+492 -169
View File
@@ -2,7 +2,6 @@ package state
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
@@ -13,32 +12,68 @@ import (
"time"
. "github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/core/sse"
sseLib "github.com/mudler/LocalAGI/core/sse"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/llm"
"github.com/mudler/LocalAGI/pkg/localrag"
"github.com/mudler/LocalAGI/pkg/utils"
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/mudler/cogito"
"github.com/mudler/xlog"
)
// SkillsProvider supplies the skills dynamic prompt and MCP session when skills are enabled for an agent.
type SkillsProvider interface {
GetSkillsPrompt(config *AgentConfig) (DynamicPrompt, error)
GetMCPSession(ctx context.Context) (*mcp.ClientSession, error)
}
// RAGProvider returns a RAGDB and optional compaction client for a collection (e.g. agent name).
// effectiveRAGURL/Key are pool/agent defaults; implementation may use them (HTTP) or ignore them (embedded).
type RAGProvider func(collectionName, effectiveRAGURL, effectiveRAGKey string) (RAGDB, KBCompactionClient, bool)
// NewHTTPRAGProvider returns a RAGProvider that uses the LocalRAG HTTP API. When effective URL/key are empty, baseURL/baseKey are used.
func NewHTTPRAGProvider(baseURL, baseKey string) RAGProvider {
return func(collectionName, effectiveURL, effectiveKey string) (RAGDB, KBCompactionClient, bool) {
url := effectiveURL
if url == "" {
url = baseURL
}
key := effectiveKey
if key == "" {
key = baseKey
}
wc := localrag.NewWrappedClient(url, key, collectionName)
return wc, &wrappedClientCompactionAdapter{WrappedClient: wc}, true
}
}
type AgentPool struct {
sync.Mutex
file string
pooldir string
pool AgentPoolData
agents map[string]*Agent
managers map[string]sse.Manager
agentStatus map[string]*Status
apiURL, defaultModel, defaultMultimodalModel string
imageModel, localRAGAPI, localRAGKey, apiKey string
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action
connectors func(*AgentConfig) []Connector
dynamicPrompt func(*AgentConfig) []DynamicPrompt
timeout string
conversationLogs string
file string
pooldir string
pool AgentPoolData
agents map[string]*Agent
managers map[string]sseLib.Manager
agentStatus map[string]*Status
apiURL, defaultModel, defaultMultimodalModel, defaultTTSModel string
defaultTranscriptionModel, defaultTranscriptionLanguage string
apiKey string
ragProvider RAGProvider
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action
connectors func(*AgentConfig) []Connector
dynamicPrompt func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []DynamicPrompt
filters func(*AgentConfig) types.JobFilters
timeout string
conversationLogs string
skillsService SkillsProvider
}
// SetRAGProvider sets the single RAG provider (HTTP or embedded). Must be called after pool creation.
func (a *AgentPool) SetRAGProvider(fn RAGProvider) {
a.Lock()
defer a.Unlock()
a.ragProvider = fn
}
type Status struct {
@@ -72,13 +107,14 @@ func loadPoolFromFile(path string) (*AgentPoolData, error) {
}
func NewAgentPool(
defaultModel, defaultMultimodalModel, imageModel, apiURL, apiKey, directory string,
LocalRAGAPI string,
defaultModel, defaultMultimodalModel, defaultTranscriptionModel, defaultTranscriptionLanguage, defaultTTSModel, apiURL, apiKey, directory string,
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action,
connectors func(*AgentConfig) []Connector,
promptBlocks func(*AgentConfig) []DynamicPrompt,
promptBlocks func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []DynamicPrompt,
filters func(*AgentConfig) types.JobFilters,
timeout string,
withLogs bool,
skillsService SkillsProvider,
) (*AgentPool, error) {
// if file exists, try to load an existing pool.
// if file does not exist, create a new pool.
@@ -89,52 +125,67 @@ func NewAgentPool(
if withLogs {
conversationPath = filepath.Join(directory, "conversations")
}
if _, err := os.Stat(poolfile); err != nil {
// file does not exist, create a new pool
return &AgentPool{
file: poolfile,
pooldir: directory,
apiURL: apiURL,
defaultModel: defaultModel,
defaultMultimodalModel: defaultMultimodalModel,
imageModel: imageModel,
localRAGAPI: LocalRAGAPI,
apiKey: apiKey,
agents: make(map[string]*Agent),
pool: make(map[string]AgentConfig),
agentStatus: make(map[string]*Status),
managers: make(map[string]sse.Manager),
connectors: connectors,
availableActions: availableActions,
dynamicPrompt: promptBlocks,
timeout: timeout,
conversationLogs: conversationPath,
file: poolfile,
pooldir: directory,
apiURL: apiURL,
defaultModel: defaultModel,
defaultMultimodalModel: defaultMultimodalModel,
defaultTranscriptionModel: defaultTranscriptionModel,
defaultTranscriptionLanguage: defaultTranscriptionLanguage,
defaultTTSModel: defaultTTSModel,
apiKey: apiKey,
agents: make(map[string]*Agent),
pool: make(map[string]AgentConfig),
agentStatus: make(map[string]*Status),
managers: make(map[string]sseLib.Manager),
connectors: connectors,
availableActions: availableActions,
dynamicPrompt: promptBlocks,
filters: filters,
timeout: timeout,
conversationLogs: conversationPath,
skillsService: skillsService,
}, nil
}
poolData, err := loadPoolFromFile(poolfile)
if err != nil {
return nil, err
bakPath := poolfile + ".bak"
poolData, err = loadPoolFromFile(bakPath)
if err != nil {
xlog.Warn("Pool file invalid and backup missing or invalid, starting with empty pool", "poolfile", poolfile, "error", err)
poolData = &AgentPoolData{}
} else {
xlog.Info("Recovered pool from backup, repairing main file", "poolfile", poolfile)
if repairData, _ := json.MarshalIndent(poolData, "", " "); len(repairData) > 0 {
_ = os.WriteFile(poolfile, repairData, 0644)
}
}
}
return &AgentPool{
file: poolfile,
apiURL: apiURL,
pooldir: directory,
defaultModel: defaultModel,
defaultMultimodalModel: defaultMultimodalModel,
imageModel: imageModel,
apiKey: apiKey,
agents: make(map[string]*Agent),
managers: make(map[string]sse.Manager),
agentStatus: map[string]*Status{},
pool: *poolData,
connectors: connectors,
localRAGAPI: LocalRAGAPI,
dynamicPrompt: promptBlocks,
availableActions: availableActions,
timeout: timeout,
conversationLogs: conversationPath,
file: poolfile,
apiURL: apiURL,
pooldir: directory,
defaultModel: defaultModel,
defaultMultimodalModel: defaultMultimodalModel,
defaultTranscriptionModel: defaultTranscriptionModel,
defaultTranscriptionLanguage: defaultTranscriptionLanguage,
defaultTTSModel: defaultTTSModel,
apiKey: apiKey,
agents: make(map[string]*Agent),
managers: make(map[string]sseLib.Manager),
agentStatus: map[string]*Status{},
pool: *poolData,
connectors: connectors,
dynamicPrompt: promptBlocks,
filters: filters,
availableActions: availableActions,
timeout: timeout,
conversationLogs: conversationPath,
skillsService: skillsService,
}, nil
}
@@ -143,6 +194,14 @@ func replaceInvalidChars(s string) string {
return strings.ReplaceAll(s, " ", "_")
}
// StartAgentStandalone starts an agent without saving it to the pool registry.
// It is intended for running a single agent from the CLI without the web server.
func (a *AgentPool) StartAgentStandalone(name string, agentConfig *AgentConfig) error {
a.Lock()
defer a.Unlock()
return a.startAgentWithConfig(name, a.pooldir, agentConfig, nil)
}
// CreateAgent adds a new agent to the pool
// and starts it.
// It also saves the state to the file.
@@ -159,92 +218,56 @@ func (a *AgentPool) CreateAgent(name string, agentConfig *AgentConfig) error {
return err
}
go func(ac AgentConfig) {
// Create the agent avatar
if err := createAgentAvatar(a.apiURL, a.apiKey, a.defaultModel, a.imageModel, a.pooldir, ac); err != nil {
xlog.Error("Failed to create agent avatar", "error", err)
}
}(a.pool[name])
return a.startAgentWithConfig(name, agentConfig)
return a.startAgentWithConfig(name, a.pooldir, agentConfig, nil)
}
func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agent AgentConfig) error {
client := llm.NewClient(APIKey, APIURL+"/v1", "10m")
func (a *AgentPool) RecreateAgent(name string, agentConfig *AgentConfig) error {
a.Lock()
defer a.Unlock()
if imageModel == "" {
return fmt.Errorf("image model not set")
oldAgent := a.agents[name]
var o *types.Observable
var obs Observer
if oldAgent != nil {
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()
}
if model == "" {
return fmt.Errorf("default model not set")
}
a.pool[name] = *agentConfig
delete(a.agents, name)
imagePath := filepath.Join(avatarDir, "avatars", fmt.Sprintf("%s.png", agent.Name))
if _, err := os.Stat(imagePath); err == nil {
// Image already exists
xlog.Debug("Avatar already exists", "path", imagePath)
return nil
}
var results struct {
ImagePrompt string `json:"image_prompt"`
}
err := llm.GenerateTypedJSON(
context.Background(),
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,
model,
jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"image_prompt": {
Type: jsonschema.String,
Description: "The prompt to generate the image",
},
},
Required: []string{"image_prompt"},
}, &results)
if err != nil {
return fmt.Errorf("failed to generate image prompt: %w", err)
}
if results.ImagePrompt == "" {
xlog.Error("Failed to generate image prompt")
return fmt.Errorf("failed to generate image prompt")
}
req := openai.ImageRequest{
Prompt: results.ImagePrompt,
Model: imageModel,
Size: openai.CreateImageSize256x256,
ResponseFormat: openai.CreateImageResponseFormatB64JSON,
}
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
resp, err := client.CreateImage(ctx, req)
if err != nil {
return fmt.Errorf("failed to generate image: %w", err)
}
if len(resp.Data) == 0 {
return fmt.Errorf("failed to generate image")
}
imageJson := resp.Data[0].B64JSON
os.MkdirAll(filepath.Join(avatarDir, "avatars"), 0755)
// Save the image to the agent directory
imageData, err := base64.StdEncoding.DecodeString(imageJson)
if err != nil {
if err := a.save(); err != nil {
if obs != nil {
o.Completion = &types.Completion{Error: err.Error()}
obs.Update(*o)
}
return err
}
return os.WriteFile(imagePath, imageData, 0644)
if err := a.startAgentWithConfig(name, a.pooldir, 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 (a *AgentPool) List() []string {
@@ -268,43 +291,74 @@ func (a *AgentPool) GetStatusHistory(name string) *Status {
return a.agentStatus[name]
}
func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error {
manager := sse.NewManager(5)
func (a *AgentPool) startAgentWithConfig(name, pooldir string, config *AgentConfig, obs Observer) error {
var manager sseLib.Manager
if m, ok := a.managers[name]; ok {
manager = m
} else {
manager = sseLib.NewManager(5)
}
ctx := context.Background()
model := a.defaultModel
multimodalModel := a.defaultMultimodalModel
transcriptionModel := a.defaultTranscriptionModel
transcriptionLanguage := a.defaultTranscriptionLanguage
ttsModel := a.defaultTTSModel
if config.MultimodalModel != "" {
multimodalModel = config.MultimodalModel
}
if config.TranscriptionModel != "" {
transcriptionModel = config.TranscriptionModel
}
if config.TranscriptionLanguage != "" {
transcriptionLanguage = config.TranscriptionLanguage
}
if config.TTSModel != "" {
ttsModel = config.TTSModel
}
if config.Model != "" {
model = config.Model
} else {
config.Model = model
}
if config.PeriodicRuns == "" {
config.PeriodicRuns = "10m"
}
if config.SchedulerPollInterval == "" {
config.SchedulerPollInterval = "30s"
}
// Use agent-specific config when set, otherwise pool defaults. Do not update pool from agent config.
effectiveAPIURL := a.apiURL
if config.APIURL != "" {
a.apiURL = config.APIURL
effectiveAPIURL = config.APIURL
} else {
config.APIURL = a.apiURL
}
effectiveAPIKey := a.apiKey
if config.APIKey != "" {
a.apiKey = config.APIKey
}
if config.LocalRAGURL != "" {
a.localRAGAPI = config.LocalRAGURL
}
if config.LocalRAGAPIKey != "" {
a.localRAGKey = config.LocalRAGAPIKey
effectiveAPIKey = config.APIKey
} else {
config.APIKey = a.apiKey
}
effectiveLocalRAGAPI := config.LocalRAGURL
effectiveLocalRAGKey := config.LocalRAGAPIKey
connectors := a.connectors(config)
promptBlocks := a.dynamicPrompt(config)
promptBlocks := a.dynamicPrompt(config)(ctx, a)
if a.skillsService != nil && config.EnableSkills {
if prompt, err := a.skillsService.GetSkillsPrompt(config); err == nil && prompt != nil {
promptBlocks = append(promptBlocks, prompt)
}
}
actions := a.availableActions(config)(ctx, a)
filters := a.filters(config)
stateFile, characterFile := a.stateFiles(name)
actionsLog := []string{}
@@ -317,13 +371,19 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
connectorLog = append(connectorLog, fmt.Sprintf("%+v", connector))
}
filtersLog := []string{}
for _, filter := range filters {
filtersLog = append(filtersLog, filter.Name())
}
xlog.Info(
"Creating agent",
"name", name,
"model", model,
"api_url", a.apiURL,
"api_url", effectiveAPIURL,
"actions", actionsLog,
"connectors", connectorLog,
"filters", filtersLog,
)
// dynamicPrompts := []map[string]string{}
@@ -331,14 +391,26 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
// dynamicPrompts = append(dynamicPrompts, p.ToMap())
// }
if obs == nil {
obs = NewSSEObserver(name, manager)
}
opts := []Option{
WithSchedulerStorePath(filepath.Join(pooldir, fmt.Sprintf("scheduler-%s.json", name))),
WithModel(model),
WithLLMAPIURL(a.apiURL),
WithLLMAPIURL(effectiveAPIURL),
WithContext(ctx),
WithMCPServers(config.MCPServers...),
WithTranscriptionModel(transcriptionModel),
WithTranscriptionLanguage(transcriptionLanguage),
WithTTSModel(ttsModel),
WithPeriodicRuns(config.PeriodicRuns),
WithSchedulerPollInterval(config.SchedulerPollInterval),
WithPermanentGoal(config.PermanentGoal),
WithMCPSTDIOServers(config.MCPSTDIOServers...),
WithPrompts(promptBlocks...),
WithJobFilters(filters...),
WithMCPPrepareScript(config.MCPPrepareScript),
// WithDynamicPrompts(dynamicPrompts...),
WithCharacter(Character{
Name: name,
@@ -348,20 +420,23 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
),
WithStateFile(stateFile),
WithCharacterFile(characterFile),
WithLLMAPIKey(a.apiKey),
WithLLMAPIKey(effectiveAPIKey),
WithTimeout(a.timeout),
WithRAGDB(localrag.NewWrappedClient(a.localRAGAPI, a.localRAGKey, name)),
WithAgentReasoningCallback(func(state types.ActionCurrentState) bool {
var actionName types.ActionDefinitionName
if state.Action != nil {
actionName = state.Action.Definition().Name
}
xlog.Info(
"Agent is thinking",
"agent", name,
"reasoning", state.Reasoning,
"action", state.Action.Definition().Name,
"action", actionName,
"params", state.Params,
)
manager.Send(
sse.NewMessage(
sseLib.NewMessage(
fmt.Sprintf(`Thinking: %s`, utils.HTMLify(state.Reasoning)),
).WithEvent("status"),
)
@@ -374,7 +449,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
return true
}),
WithSystemPrompt(config.SystemPrompt),
WithInnerMonologueTemplate(config.InnerMonologueTemplate),
WithSchedulerTaskTemplate(config.SchedulerTaskTemplate),
WithMultimodalModel(multimodalModel),
WithLastMessageDuration(config.LastMessageDuration),
WithAgentResultCallback(func(state types.ActionState) {
a.Lock()
if _, ok := a.agentStatus[name]; !ok {
@@ -387,16 +465,20 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
"Calling agent result callback",
)
var actionName types.ActionDefinitionName
if state.ActionCurrentState.Action != nil {
actionName = state.ActionCurrentState.Action.Definition().Name
}
text := fmt.Sprintf(`Reasoning: %s
Action taken: %+v
Parameters: %+v
Result: %s`,
state.Reasoning,
state.ActionCurrentState.Action.Definition().Name,
actionName,
state.ActionCurrentState.Params,
state.Result)
manager.Send(
sse.NewMessage(
sseLib.NewMessage(
utils.HTMLify(
text,
),
@@ -407,6 +489,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
c.AgentResultCallback()(state)
}
}),
WithObserver(obs),
}
if config.HUD {
@@ -429,6 +512,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
opts = append(opts, EnableSummaryMemory)
}
if config.ConversationStorageMode != "" {
opts = append(opts, WithConversationStorageMode(ConversationStorageMode(config.ConversationStorageMode)))
}
if config.CanStopItself {
opts = append(opts, CanStopItself)
}
@@ -437,6 +524,14 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
opts = append(opts, EnablePlanning)
}
if config.PlanReviewerModel != "" {
opts = append(opts, WithPlanReviewerLLM(config.PlanReviewerModel))
}
if config.DisableSinkState {
opts = append(opts, DisableSinkState)
}
if config.InitiateConversations {
opts = append(opts, EnableInitiateConversations)
}
@@ -449,22 +544,125 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
}
}
if config.EnableKnowledgeBase {
opts = append(opts, EnableKnowledgeBase)
if a.skillsService != nil && config.EnableSkills {
if session, err := a.skillsService.GetMCPSession(ctx); err == nil && session != nil {
opts = append(opts, WithMCPSession(session))
}
}
var ragDB RAGDB
var compactionClient KBCompactionClient
if config.EnableKnowledgeBase && a.ragProvider != nil {
if db, comp, ok := a.ragProvider(name, effectiveLocalRAGAPI, effectiveLocalRAGKey); ok && db != nil {
ragDB = db
compactionClient = comp
}
}
if ragDB != nil {
opts = append(opts, WithRAGDB(ragDB), EnableKnowledgeBase)
kbAutoSearch := config.KBAutoSearch
if !config.KBAutoSearch && !config.KBAsTools {
kbAutoSearch = true
}
opts = append(opts, WithKBAutoSearch(kbAutoSearch))
if config.KBAsTools {
kbResults := config.KnowledgeBaseResults
if kbResults <= 0 {
kbResults = 5
}
searchAction, addAction := NewKBWrapperActions(ragDB, kbResults)
opts = append(opts, WithActions(searchAction, addAction))
}
}
if config.EnableReasoning {
opts = append(opts, EnableForceReasoning)
}
if config.EnableGuidedTools {
opts = append(opts, EnableGuidedTools)
}
if config.StripThinkingTags {
opts = append(opts, EnableStripThinkingTags)
}
if config.EnableAutoCompaction {
opts = append(opts, EnableAutoCompaction)
}
if config.AutoCompactionThreshold > 0 {
opts = append(opts, WithAutoCompactionThreshold(config.AutoCompactionThreshold))
}
if config.KnowledgeBaseResults > 0 {
opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults))
}
if config.LoopDetectionSteps > 0 {
opts = append(opts, WithLoopDetectionSteps(config.LoopDetectionSteps))
if config.ParallelJobs > 0 {
opts = append(opts, WithParallelJobs(config.ParallelJobs))
}
if config.CancelPreviousOnNewMessage != nil {
opts = append(opts, WithCancelPreviousOnNewMessage(*config.CancelPreviousOnNewMessage))
} else {
opts = append(opts, WithCancelPreviousOnNewMessage(true))
}
if config.EnableEvaluation {
opts = append(opts, EnableEvaluation())
}
if config.MaxEvaluationLoops > 0 {
opts = append(opts, WithMaxEvaluationLoops(config.MaxEvaluationLoops))
}
if config.MaxAttempts > 0 {
opts = append(opts, WithMaxAttempts(config.MaxAttempts))
}
if config.LoopDetection > 0 {
opts = append(opts, WithLoopDetection(config.LoopDetection))
}
if config.EnableForceReasoningTool {
opts = append(opts, EnableForceReasoningTool)
}
// Wire cogito streaming events into the SSE manager for live token delivery
opts = append(opts, WithStreamCallback(func(ev cogito.StreamEvent) {
switch ev.Type {
case cogito.StreamEventReasoning:
data, _ := json.Marshal(map[string]interface{}{
"type": "reasoning",
"content": ev.Content,
"timestamp": time.Now().Format(time.RFC3339),
})
manager.Send(sseLib.NewMessage(string(data)).WithEvent("stream_event"))
case cogito.StreamEventContent:
data, _ := json.Marshal(map[string]interface{}{
"type": "content",
"content": ev.Content,
"timestamp": time.Now().Format(time.RFC3339),
})
manager.Send(sseLib.NewMessage(string(data)).WithEvent("stream_event"))
case cogito.StreamEventToolCall:
data, _ := json.Marshal(map[string]interface{}{
"type": "tool_call",
"tool_name": ev.ToolName,
"tool_args": ev.ToolArgs,
"timestamp": time.Now().Format(time.RFC3339),
})
manager.Send(sseLib.NewMessage(string(data)).WithEvent("stream_event"))
case cogito.StreamEventDone:
data, _ := json.Marshal(map[string]interface{}{
"type": "done",
"timestamp": time.Now().Format(time.RFC3339),
})
manager.Send(sseLib.NewMessage(string(data)).WithEvent("stream_event"))
}
}))
xlog.Info("Starting agent", "name", name, "config", config)
agent, err := New(opts...)
@@ -481,6 +679,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
}
}()
if config.EnableKnowledgeBase && config.EnableKBCompaction && compactionClient != nil {
go runCompactionTicker(ctx, compactionClient, config, effectiveAPIURL, effectiveAPIKey, model)
}
xlog.Info("Starting connectors", "name", name, "config", config)
for _, c := range connectors {
@@ -490,7 +692,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
go func() {
for {
time.Sleep(1 * time.Second) // Send a message every seconds
manager.Send(sse.NewMessage(
manager.Send(sseLib.NewMessage(
utils.HTMLify(agent.State().String()),
).WithEvent("hud"))
}
@@ -509,7 +711,7 @@ func (a *AgentPool) StartAll() error {
if a.agents[name] != nil { // Agent already started
continue
}
if err := a.startAgentWithConfig(name, &config); err != nil {
if err := a.startAgentWithConfig(name, a.pooldir, &config, nil); err != nil {
xlog.Error("Failed to start agent", "name", name, "error", err)
}
}
@@ -547,12 +749,121 @@ func (a *AgentPool) Start(name string) error {
return nil
}
if config, ok := a.pool[name]; ok {
return a.startAgentWithConfig(name, &config)
return a.startAgentWithConfig(name, a.pooldir, &config, nil)
}
return fmt.Errorf("agent %s not found", name)
}
// CreateOnly creates the agent instance without calling Run().
// This is used in distributed mode where the agent is executed statelessly
// via AskDirect() — the persistent Run() loop is not needed.
func (a *AgentPool) CreateOnly(name string) error {
a.Lock()
defer a.Unlock()
if _, ok := a.agents[name]; ok {
return nil // already created
}
if config, ok := a.pool[name]; ok {
return a.createAgentWithoutRun(name, a.pooldir, &config)
}
return fmt.Errorf("agent %s not found", name)
}
// createAgentWithoutRun is like startAgentWithConfig but skips Run(), connectors, and HUD.
func (a *AgentPool) createAgentWithoutRun(name, pooldir string, config *AgentConfig) error {
var manager sseLib.Manager
if m, ok := a.managers[name]; ok {
manager = m
} else {
manager = sseLib.NewManager(5)
}
ctx := context.Background()
model := a.defaultModel
multimodalModel := a.defaultMultimodalModel
transcriptionModel := a.defaultTranscriptionModel
transcriptionLanguage := a.defaultTranscriptionLanguage
ttsModel := a.defaultTTSModel
if config.MultimodalModel != "" {
multimodalModel = config.MultimodalModel
}
if config.TranscriptionModel != "" {
transcriptionModel = config.TranscriptionModel
}
if config.TranscriptionLanguage != "" {
transcriptionLanguage = config.TranscriptionLanguage
}
if config.TTSModel != "" {
ttsModel = config.TTSModel
}
if config.Model != "" {
model = config.Model
} else {
config.Model = model
}
effectiveAPIURL := a.apiURL
if config.APIURL != "" {
effectiveAPIURL = config.APIURL
} else {
config.APIURL = a.apiURL
}
effectiveAPIKey := a.apiKey
if config.APIKey != "" {
effectiveAPIKey = config.APIKey
} else {
config.APIKey = a.apiKey
}
promptBlocks := a.dynamicPrompt(config)(ctx, a)
if a.skillsService != nil && config.EnableSkills {
if prompt, err := a.skillsService.GetSkillsPrompt(config); err == nil && prompt != nil {
promptBlocks = append(promptBlocks, prompt)
}
}
actions := a.availableActions(config)(ctx, a)
stateFile, characterFile := a.stateFiles(name)
obs := NewSSEObserver(name, manager)
opts := []Option{
WithSchedulerStorePath(filepath.Join(pooldir, fmt.Sprintf("scheduler-%s.json", name))),
WithModel(model),
WithLLMAPIURL(effectiveAPIURL),
WithContext(ctx),
WithTranscriptionModel(transcriptionModel),
WithTranscriptionLanguage(transcriptionLanguage),
WithTTSModel(ttsModel),
WithPrompts(promptBlocks...),
WithActions(actions...),
WithObserver(obs),
WithMultimodalModel(multimodalModel),
WithCharacterFile(characterFile),
WithStateFile(stateFile),
WithSystemPrompt(config.SystemPrompt),
}
if effectiveAPIKey != "" {
opts = append(opts, WithLLMAPIKey(effectiveAPIKey))
}
xlog.Info("Creating agent (no Run)", "name", name, "model", model, "api_url", effectiveAPIURL)
agent, err := New(opts...)
if err != nil {
return err
}
a.agents[name] = agent
a.managers[name] = manager
// Start the conversation consumer so ConversationAction doesn't deadlock.
// This is normally started by Run(), but we skip Run() in distributed mode.
agent.StartConversationConsumer()
xlog.Info("Agent created (no Run)", "name", name)
return nil
}
func (a *AgentPool) stateFiles(name string) (string, string) {
stateFile := filepath.Join(a.pooldir, fmt.Sprintf("%s.state.json", name))
characterFile := filepath.Join(a.pooldir, fmt.Sprintf("%s.character.json", name))
@@ -573,9 +884,6 @@ func (a *AgentPool) Remove(name string) error {
delete(a.agents, name)
delete(a.pool, name)
// remove avatar
os.Remove(filepath.Join(a.pooldir, "avatars", fmt.Sprintf("%s.png", name)))
if err := a.save(); err != nil {
return err
}
@@ -593,7 +901,21 @@ func (a *AgentPool) save() error {
if err != nil {
return err
}
return os.WriteFile(a.file, data, 0644)
tmpPath := a.file + ".tmp"
if err := os.WriteFile(tmpPath, data, 0644); err != nil {
os.Remove(tmpPath)
return err
}
if err := os.Rename(tmpPath, a.file); err != nil {
os.Remove(tmpPath)
return err
}
bakPath := a.file + ".bak"
if err := os.WriteFile(bakPath, data, 0644); err != nil {
// best-effort; main file is already good
xlog.Warn("Failed to write pool backup", "path", bakPath, "error", err)
}
return nil
}
func (a *AgentPool) GetAgent(name string) *Agent {
@@ -622,8 +944,9 @@ func (a *AgentPool) GetConfig(name string) *AgentConfig {
return &agent
}
func (a *AgentPool) GetManager(name string) sse.Manager {
func (a *AgentPool) GetManager(name string) sseLib.Manager {
a.Lock()
defer a.Unlock()
return a.managers[name]
}
+95 -5
View File
@@ -3,7 +3,9 @@ package types
import (
"context"
"encoding/json"
"fmt"
"github.com/mudler/cogito"
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
)
@@ -29,9 +31,10 @@ func NewActionContext(ctx context.Context, cancel context.CancelFunc) *ActionCon
type ActionParams map[string]interface{}
type ActionResult struct {
Job *Job
Result string
Metadata map[string]interface{}
Job *Job
Result string
ImageBase64Result string
Metadata map[string]interface{}
}
func (ap ActionParams) Read(s string) error {
@@ -86,11 +89,90 @@ func (a ActionDefinition) ToFunctionDefinition() *openai.FunctionDefinition {
}
}
type cogitoWrapper struct {
action Action
ctx context.Context
sharedState *AgentSharedState
}
func (c *cogitoWrapper) Tool() openai.Tool {
return openai.Tool{
Type: openai.ToolTypeFunction,
Function: c.action.Definition().ToFunctionDefinition(),
}
}
func (c *cogitoWrapper) Execute(args map[string]any) (string, any, error) {
ctx := c.ctx
if ctx == nil {
ctx = context.Background()
}
result, err := c.action.Run(ctx, c.sharedState, ActionParams(args))
if err != nil {
return "", nil, err
}
return result.Result, result, nil
}
// Actions is something the agent can do
type Action interface {
Run(ctx context.Context, action ActionParams) (ActionResult, error)
Run(ctx context.Context, sharedState *AgentSharedState, action ActionParams) (ActionResult, error)
Definition() ActionDefinition
Plannable() bool
}
// UserDefinedChecker interface to identify user-defined actions
type UserDefinedChecker interface {
IsUserDefined() bool
}
// BaseAction provides default implementation for Action interface
// Embed this in action implementations to get the default IsUserDefined behavior
type BaseAction struct{}
func (b *BaseAction) IsUserDefined() bool {
return false // Regular actions are not user-defined
}
// IsActionUserDefined checks if an action is user-defined
func IsActionUserDefined(action Action) bool {
if checker, ok := action.(UserDefinedChecker); ok {
return checker.IsUserDefined()
}
return false // Actions without UserDefinedChecker are not user-defined
}
// UserDefinedAction represents a user-defined function tool
type UserDefinedAction struct {
ActionDef *ActionDefinition
}
func (u *UserDefinedAction) Run(ctx context.Context, sharedState *AgentSharedState, action ActionParams) (ActionResult, error) {
// User-defined actions should not be executed directly
return ActionResult{}, fmt.Errorf("user-defined action '%s' cannot be executed by agent", u.ActionDef.Name)
}
func (u *UserDefinedAction) Definition() ActionDefinition {
return *u.ActionDef
}
func (u *UserDefinedAction) Plannable() bool {
return true // User-defined actions are plannable
}
func (u *UserDefinedAction) IsUserDefined() bool {
return true
}
// CreateUserDefinedActions converts user tools to UserDefinedAction instances
func CreateUserDefinedActions(userTools []ActionDefinition) []Action {
var actions []Action
for _, tool := range userTools {
actions = append(actions, &UserDefinedAction{
ActionDef: &tool,
})
}
return actions
}
type Actions []Action
@@ -106,6 +188,14 @@ func (a Actions) ToTools() []openai.Tool {
return tools
}
func (a Actions) ToCogitoTools(ctx context.Context, sharedState *AgentSharedState) []cogito.ToolDefinitionInterface {
tools := []cogito.ToolDefinitionInterface{}
for _, action := range a {
tools = append(tools, &cogitoWrapper{action: action, ctx: ctx, sharedState: sharedState})
}
return tools
}
func (a Actions) Find(name string) Action {
for _, action := range a {
if action.Definition().Name.Is(name) {
+27
View File
@@ -0,0 +1,27 @@
package types
import (
"github.com/sashabaranov/go-openai"
)
// ConversationMessage represents a message with associated metadata
// Used when the agent initiates new conversations to preserve context
// such as generated images, files, or URLs
type ConversationMessage struct {
Message openai.ChatCompletionMessage
Metadata map[string]interface{}
}
// NewConversationMessage creates a new ConversationMessage with the given message
func NewConversationMessage(msg openai.ChatCompletionMessage) *ConversationMessage {
return &ConversationMessage{
Message: msg,
Metadata: make(map[string]interface{}),
}
}
// WithMetadata adds metadata to the conversation message
func (c *ConversationMessage) WithMetadata(metadata map[string]interface{}) *ConversationMessage {
c.Metadata = metadata
return c
}
+15
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"`
}
+91 -50
View File
@@ -5,9 +5,15 @@ import (
"log"
"github.com/google/uuid"
"github.com/mudler/cogito"
"github.com/sashabaranov/go-openai"
)
// MetadataKeyConversationID is the job metadata key for per-conversation identity.
// When set (e.g. "slack:CHANNEL_ID", "telegram:CHAT_ID"), the agent may cancel the
// currently running job for that conversation before enqueueing a new one.
const MetadataKeyConversationID = "conversation_id"
// Job is a request to the agent to do something
type Job struct {
// The job is a request to the agent to do something
@@ -19,14 +25,18 @@ type Job struct {
ConversationHistory []openai.ChatCompletionMessage
UUID string
Metadata map[string]interface{}
DoneFilter bool
pastActions []*ActionRequest
nextAction *Action
nextActionParams *ActionParams
nextActionReasoning string
// Tools available for this job
BuiltinTools []ActionDefinition // Built-in tools like web search
UserTools []ActionDefinition // User-defined function tools
ToolChoice string
context context.Context
cancel context.CancelFunc
context context.Context
fragment *cogito.Fragment
cancel context.CancelFunc
Obs *Observable
}
type ActionRequest struct {
@@ -42,6 +52,24 @@ func WithConversationHistory(history []openai.ChatCompletionMessage) JobOption {
}
}
func WithBuiltinTools(tools []ActionDefinition) JobOption {
return func(j *Job) {
j.BuiltinTools = tools
}
}
func WithUserTools(tools []ActionDefinition) JobOption {
return func(j *Job) {
j.UserTools = tools
}
}
func WithToolChoice(choice string) JobOption {
return func(j *Job) {
j.ToolChoice = choice
}
}
func WithReasoningCallback(f func(ActionCurrentState) bool) JobOption {
return func(r *Job) {
r.ReasoningCallback = f
@@ -54,7 +82,7 @@ func WithResultCallback(f func(ActionState)) JobOption {
}
}
func WithMetadata(metadata map[string]interface{}) JobOption {
func WithMetadata(metadata map[string]any) JobOption {
return func(j *Job) {
j.Metadata = metadata
}
@@ -82,37 +110,6 @@ func (j *Job) CallbackWithResult(stateResult ActionState) {
j.ResultCallback(stateResult)
}
func (j *Job) SetNextAction(action *Action, params *ActionParams, reasoning string) {
j.nextAction = action
j.nextActionParams = params
j.nextActionReasoning = reasoning
}
func (j *Job) AddPastAction(action Action, params *ActionParams) {
j.pastActions = append(j.pastActions, &ActionRequest{
Action: action,
Params: params,
})
}
func (j *Job) GetPastActions() []*ActionRequest {
return j.pastActions
}
func (j *Job) GetNextAction() (*Action, *ActionParams, string) {
return j.nextAction, j.nextActionParams, j.nextActionReasoning
}
func (j *Job) HasNextAction() bool {
return j.nextAction != nil
}
func (j *Job) ResetNextAction() {
j.nextAction = nil
j.nextActionParams = nil
j.nextActionReasoning = ""
}
func WithTextImage(text, image string) JobOption {
return func(j *Job) {
j.ConversationHistory = append(j.ConversationHistory, openai.ChatCompletionMessage{
@@ -159,23 +156,23 @@ func newUUID() string {
// To wait for a Job result, use JobResult.WaitResult()
func NewJob(opts ...JobOption) *Job {
j := &Job{
Result: NewJobResult(),
UUID: newUUID(),
}
for _, o := range opts {
o(j)
Result: NewJobResult(),
UUID: uuid.New().String(),
Metadata: make(map[string]interface{}),
context: context.Background(),
ConversationHistory: []openai.ChatCompletionMessage{},
}
var ctx context.Context
if j.context == nil {
ctx = context.Background()
} else {
ctx = j.context
for _, opt := range opts {
opt(j)
}
context, cancel := context.WithCancel(ctx)
j.context = context
// Store the original request if it exists in the conversation history
ctx, cancel := context.WithCancel(j.context)
j.context = ctx
j.cancel = cancel
return j
}
@@ -198,3 +195,47 @@ func (j *Job) Cancel() {
func (j *Job) GetContext() context.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
}
// GetBuiltinTools returns the builtin tools for this job
func (j *Job) GetBuiltinTools() []ActionDefinition {
return j.BuiltinTools
}
// GetUserTools returns the user tools for this job
func (j *Job) GetUserTools() []ActionDefinition {
return j.UserTools
}
// GetAllTools returns all tools (builtin + user) for this job
func (j *Job) GetAllTools() []ActionDefinition {
allTools := make([]ActionDefinition, 0, len(j.BuiltinTools)+len(j.UserTools))
allTools = append(allTools, j.BuiltinTools...)
allTools = append(allTools, j.UserTools...)
return allTools
}
+63
View File
@@ -0,0 +1,63 @@
package types
import (
"github.com/mudler/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,
}
}
+6
View File
@@ -0,0 +1,6 @@
package types
type PromptResult struct {
Content string
ImageBase64 string
}
+11 -4
View File
@@ -1,8 +1,10 @@
package types
import (
"context"
"sync"
"github.com/mudler/cogito"
"github.com/sashabaranov/go-openai"
)
@@ -11,6 +13,7 @@ type JobResult struct {
sync.Mutex
// The result of a job
State []ActionState
Plans []cogito.PlanStatus
Conversation []openai.ChatCompletionMessage
Finalizers []func([]openai.ChatCompletionMessage)
@@ -28,7 +31,7 @@ func (j *JobResult) SetResult(text ActionState) {
j.State = append(j.State, text)
}
// SetResult sets the result of a job
// Finish marks the job as done and closes the ready channel.
func (j *JobResult) Finish(e error) {
j.Lock()
j.Error = e
@@ -59,9 +62,13 @@ func (j *JobResult) SetResponse(response string) {
}
// WaitResult waits for the result of a job
func (j *JobResult) WaitResult() *JobResult {
<-j.ready
func (j *JobResult) WaitResult(ctx context.Context) (*JobResult, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-j.ready:
}
j.Lock()
defer j.Unlock()
return j
return j, nil
}
+92
View File
@@ -0,0 +1,92 @@
package types
import (
"fmt"
"time"
"github.com/mudler/LocalAGI/core/conversations"
"github.com/mudler/LocalAGI/core/scheduler"
)
// Forward declaration to avoid circular import
type TaskScheduler interface {
CreateTask(task *scheduler.Task) error
GetAllTasks() ([]*scheduler.Task, error)
GetTask(id string) (*scheduler.Task, error)
DeleteTask(id string) error
PauseTask(id string) error
ResumeTask(id string) error
}
// 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
)
// RecurringReminderParams are the parameters the LLM provides for set_recurring_reminder.
type RecurringReminderParams struct {
Message string `json:"message"`
CronExpr string `json:"cron_expr"`
}
// OneTimeReminderParams are the parameters the LLM provides for set_onetime_reminder.
type OneTimeReminderParams struct {
Message string `json:"message"`
Delay string `json:"delay"` // Go duration format with day support: "30m", "2h", "1d", "1d12h"
}
// ReminderActionResponse is kept for backward compatibility.
// Deprecated: use RecurringReminderParams or OneTimeReminderParams.
type ReminderActionResponse = RecurringReminderParams
type AgentSharedState struct {
ConversationTracker *conversations.ConversationTracker[string] `json:"conversation_tracker"`
Scheduler TaskScheduler `json:"-"` // Not serialized, set at runtime
AgentName string `json:"agent_name"`
}
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,
)
}
+27
View File
@@ -0,0 +1,27 @@
services:
localai:
extends:
file: docker-compose.yaml
service: localai
environment:
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
- DEBUG=true
image: localai/localai:master-gpu-hipblas
devices:
- /dev/dri
- /dev/kfd
postgres:
extends:
file: docker-compose.yaml
service: postgres
dind:
extends:
file: docker-compose.yaml
service: dind
localagi:
extends:
file: docker-compose.yaml
service: localagi
+5 -5
View File
@@ -6,21 +6,21 @@ services:
environment:
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
- DEBUG=true
image: localai/localai:master-sycl-f32-ffmpeg-core
image: localai/localai:master-gpu-intel
devices:
# On a system with integrated GPU and an Arc 770, this is the Arc 770
- /dev/dri/card1
- /dev/dri/renderD129
localrecall:
postgres:
extends:
file: docker-compose.yaml
service: localrecall
service: postgres
localrecall-healthcheck:
dind:
extends:
file: docker-compose.yaml
service: localrecall-healthcheck
service: dind
localagi:
extends:
+6 -6
View File
@@ -6,7 +6,7 @@ services:
environment:
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
- DEBUG=true
image: localai/localai:master-cublas-cuda12-ffmpeg-core
image: localai/localai:master-gpu-nvidia-cuda-12
# For images with python backends, use:
# image: localai/localai:master-cublas-cuda12-ffmpeg
deploy:
@@ -17,17 +17,17 @@ services:
count: 1
capabilities: [gpu]
localrecall:
postgres:
extends:
file: docker-compose.yaml
service: localrecall
service: postgres
localrecall-healthcheck:
dind:
extends:
file: docker-compose.yaml
service: localrecall-healthcheck
service: dind
localagi:
extends:
file: docker-compose.yaml
service: localagi
service: localagi
+67 -29
View File
@@ -5,11 +5,10 @@ services:
# Available images with CUDA, ROCm, SYCL, Vulkan
# Image list (quay.io): https://quay.io/repository/go-skynet/local-ai?tab=tags
# Image list (dockerhub): https://hub.docker.com/r/localai/localai
image: localai/localai:master-ffmpeg-core
image: localai/localai:master
command:
- ${MODEL_NAME:-arcee-agent}
- ${MULTIMODAL_MODEL:-minicpm-v-2_6}
- ${IMAGE_MODEL:-sd-1.5-ggml}
- ${MODEL_NAME:-gemma-3-4b-it-qat}
- ${MULTIMODAL_MODEL:-gemma-3-4b-it-qat}
- granite-embedding-107m-multilingual
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/readyz"]
@@ -22,36 +21,62 @@ services:
- DEBUG=true
#- LOCALAI_API_KEY=sk-1234567890
volumes:
- ./volumes/models:/build/models:cached
- ./volumes/images:/tmp/generated/images
- models:/models
- backends:/backends
- images:/tmp/generated/images
localrecall:
image: quay.io/mudler/localrecall:main
ports:
- 8080
postgres:
image: quay.io/mudler/localrecall:${LOCALRECALL_VERSION:-v0.5.2}-postgresql
environment:
- COLLECTION_DB_PATH=/db
- EMBEDDING_MODEL=granite-embedding-107m-multilingual
- FILE_ASSETS=/assets
- OPENAI_API_KEY=sk-1234567890
- OPENAI_BASE_URL=http://localai:8080
- POSTGRES_DB=localrecall
- POSTGRES_USER=localrecall
- POSTGRES_PASSWORD=localrecall
ports:
- 5432:5432
volumes:
- ./volumes/localrag/db:/db
- ./volumes/localrag/assets/:/assets
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U localrecall"]
interval: 10s
timeout: 5s
retries: 5
localrecall-healthcheck:
sshbox:
build:
context: .
dockerfile: Dockerfile.sshbox
ports:
- "22"
environment:
- SSH_USER=root
- SSH_PASSWORD=root
- DOCKER_HOST=tcp://dind:2375
depends_on:
localrecall:
condition: service_started
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!'"]
dind:
condition: service_healthy
dind:
image: docker:dind
privileged: true
command: ["dockerd", "-H", "tcp://0.0.0.0:2375", "-H", "unix:///var/run/docker.sock"]
environment:
- DOCKER_TLS_CERTDIR=""
expose:
- 2375
healthcheck:
test: ["CMD", "docker", "info"]
interval: 10s
timeout: 5s
retries: 3
localagi:
depends_on:
localai:
condition: service_healthy
localrecall-healthcheck:
condition: service_completed_successfully
postgres:
condition: service_healthy
dind:
condition: service_healthy
build:
context: .
dockerfile: Dockerfile.webui
@@ -59,16 +84,29 @@ services:
- 8080:3000
#image: quay.io/mudler/localagi:master
environment:
- LOCALAGI_MODEL=${MODEL_NAME:-arcee-agent}
- LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-minicpm-v-2_6}
- LOCALAGI_IMAGE_MODEL=${IMAGE_MODEL:-sd-1.5-ggml}
- LOCALAGI_MODEL=${MODEL_NAME:-gemma-3-4b-it-qat}
- LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-moondream2-20250414}
- LOCALAGI_LLM_API_URL=http://localai:8080
#- LOCALAGI_LLM_API_KEY=sk-1234567890
- LOCALAGI_LOCALRAG_URL=http://localrecall:8080
- LOCALAGI_STATE_DIR=/pool
# Knowledge base (collections) with PostgreSQL by default
- VECTOR_ENGINE=postgres
- DATABASE_URL=postgresql://localrecall:localrecall@postgres:5432/localrecall?sslmode=disable
- EMBEDDING_MODEL=granite-embedding-107m-multilingual
- LOCALAGI_TIMEOUT=5m
- LOCALAGI_ENABLE_CONVERSATIONS_LOGGING=false
- LOCALAGI_SSHBOX_URL=root:root@sshbox:22
- DOCKER_HOST=tcp://dind:2375
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ./volumes/localagi/:/pool
- localagi_pool:/pool
# Optional: mount a host directory for skills (replaces the default state-dir/skills path)
# - ./skills:/pool/skills
volumes:
postgres_data:
models:
backends:
images:
localagi_pool:
+56
View File
@@ -0,0 +1,56 @@
package custom_actions
import (
"encoding/json"
)
type Params struct {
Message string `json:"message"` // field name
}
func Run(config map[string]interface{}) (string, map[string]interface{}, error) {
p := Params{}
b, err := json.Marshal(config)
if err != nil {
return "", map[string]interface{}{}, err
}
if err := json.Unmarshal(b, &p); err != nil {
return "", map[string]interface{}{}, err
}
return "Hello, " + p.Message + "!", map[string]interface{}{}, nil
}
func Description() string {
return "Send a message to the user"
}
func Definition() map[string][]string {
return map[string][]string{
"message": { // field name
"string", // type
"The message to send", // description
},
}
}
func RequiredFields() []string {
return []string{"message"} // field name
}
var config string
func Init(configuration string) error {
// Do something with the configuration that was passed-by
config = configuration
return nil
}
// DynamicPrompt
func Render() (string, string, error) {
return "Hello, " + config + "!", "", nil
}
func Role() string {
return "system" // Role for the dynamic prompt
}
-12
View File
@@ -1,12 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from RealtimeSTT import AudioToTextRecorder
def process_text(text):
print(text)
if __name__ == '__main__':
recorder = AudioToTextRecorder(wake_words="jarvis")
while True:
recorder.text(process_text)
+128 -62
View File
@@ -1,79 +1,147 @@
module github.com/mudler/LocalAGI
go 1.24
toolchain go1.24.2
go 1.26.0
require (
github.com/bwmarrin/discordgo v0.28.1
github.com/Masterminds/sprig/v3 v3.3.0
github.com/blevesearch/bleve/v2 v2.5.7
github.com/bwmarrin/discordgo v0.29.0
github.com/chasefleming/elem-go v0.30.0
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2
github.com/donseba/go-htmx v1.12.0
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
github.com/eritikass/githubmarkdownconvertergo v0.1.10
github.com/go-telegram/bot v1.14.2
github.com/gofiber/fiber/v2 v2.52.6
github.com/go-telegram/bot v1.17.0
github.com/gofiber/fiber/v2 v2.52.11
github.com/gofiber/template/html/v2 v2.1.3
github.com/google/go-github/v69 v69.2.0
github.com/google/uuid v1.6.0
github.com/metoro-io/mcp-golang v0.9.0
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0
github.com/jung-kurt/gofpdf v1.16.2
github.com/modelcontextprotocol/go-sdk v1.2.0
github.com/mudler/cogito v0.9.5-0.20260315222927-63abdec7189b
github.com/mudler/localrecall v0.6.3-0.20260618142827-d0073dd5dc32
github.com/mudler/skillserver v0.0.5-0.20260221145827-0639a82c8f49
github.com/mudler/xlog v0.0.5
github.com/onsi/ginkgo/v2 v2.28.1
github.com/onsi/gomega v1.39.1
github.com/philippgille/chromem-go v0.7.0
github.com/sashabaranov/go-openai v1.38.1
github.com/slack-go/slack v0.16.0
github.com/robfig/cron/v3 v3.0.1
github.com/sashabaranov/go-openai v1.41.2
github.com/slack-go/slack v0.17.3
github.com/spf13/cobra v1.10.2
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
github.com/tmc/langchaingo v0.1.13
github.com/tmc/langchaingo v0.1.14
github.com/traefik/yaegi v0.16.1
github.com/valyala/fasthttp v1.60.0
golang.org/x/crypto v0.37.0
github.com/valyala/fasthttp v1.68.0
golang.org/x/crypto v0.50.0
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
maunium.net/go/mautrix v0.17.0
mvdan.cc/xurls/v2 v2.6.0
)
require (
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/antchfx/htmlquery v1.3.0 // indirect
github.com/antchfx/xmlquery v1.3.17 // indirect
github.com/antchfx/xpath v1.2.4 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.8.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/gocolly/colly v1.2.0 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/JohannesKaufmann/dom v0.2.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
github.com/bits-and-blooms/bitset v1.22.0 // indirect
github.com/blevesearch/bleve_index_api v1.2.11 // indirect
github.com/blevesearch/geo v0.2.4 // indirect
github.com/blevesearch/go-faiss v1.0.26 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 // indirect
github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
github.com/blevesearch/vellum v1.1.0 // indirect
github.com/blevesearch/zapx/v11 v11.4.2 // indirect
github.com/blevesearch/zapx/v12 v12.4.2 // indirect
github.com/blevesearch/zapx/v13 v13.4.2 // indirect
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
github.com/blevesearch/zapx/v16 v16.2.8 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.16.4 // indirect
github.com/gofiber/template v1.8.3 // indirect
github.com/gofiber/utils v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/jsonschema-go v0.3.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/invopop/jsonschema v0.12.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.8.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jolestar/go-commons-pool/v2 v2.1.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kennygrant/sanitize v1.2.4 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klippa-app/go-pdfium v1.19.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/oxffaa/gopher-parse-sitemap v0.0.0-20191021113419-005d2eb1def4 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/tetratelabs/wazero v1.11.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.20.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/antchfx/htmlquery v1.3.4 // indirect
github.com/antchfx/xmlquery v1.4.4 // indirect
github.com/antchfx/xpath v1.3.4 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emersion/go-imap/v2 v2.0.0-beta.5
github.com/emersion/go-message v0.18.2
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
github.com/emersion/go-smtp v0.24.0
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gocolly/colly v1.2.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
github.com/kennygrant/sanitize v1.2.4 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/pkoukk/tiktoken-go v0.1.7 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.31.0 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/temoto/robotstxt v1.1.2 // indirect
@@ -81,17 +149,15 @@ require (
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/tools v0.31.0 // indirect
go.mau.fi/util v0.3.0 // indirect
go.starlark.net v0.0.0-20250417143717-f57e51f710eb // indirect
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa
golang.org/x/net v0.53.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.43.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
google.golang.org/protobuf v1.36.10 // indirect
maunium.net/go/maulogger/v2 v2.4.1 // indirect
)
+415 -197
View File
@@ -1,105 +1,205 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E=
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
github.com/antchfx/xmlquery v1.3.17 h1:d0qWjPp/D+vtRw7ivCwT5ApH/3CkQU8JOeo3245PpTk=
github.com/antchfx/xmlquery v1.3.17/go.mod h1:Afkq4JIeXut75taLSuI31ISJ/zeq+3jG7TunF7noreA=
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.2.4 h1:dW1HB/JxKvGtJ9WyVGJ0sIoEcqftV3SqIstujI+B9XY=
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/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
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=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ=
github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0 h1:C0/TerKdQX9Y9pbYi1EsLr5LDNANsqunyI/btpyfCg8=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0/go.mod h1:OLaKh+giepO8j7teevrNwiy/fwf8LXgoc9g7rwaE1jk=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg=
github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antchfx/htmlquery v1.3.4 h1:Isd0srPkni2iNTWCwVj/72t7uCphFeor5Q8nCzj1jdQ=
github.com/antchfx/htmlquery v1.3.4/go.mod h1:K9os0BwIEmLAvTqaNSua8tXLWRWZpocZIH73OzWQbwM=
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4=
github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8=
github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA=
github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s=
github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI=
github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY=
github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w=
github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y=
github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=
github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=
github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks=
github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0=
github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI=
github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/chasefleming/elem-go v0.30.0 h1:BlhV1ekv1RbFiM8XZUQeln1Ikb4D+bu2eDO4agREvok=
github.com/chasefleming/elem-go v0.30.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2 h1:flLYmnQFZNo04x2NPehMbf30m7Pli57xwZ0NFqR/hb0=
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2/go.mod h1:NtWqRzAp/1tw+twkW8uuBenEVVYndEAZACWU3F3xdoQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/donseba/go-htmx v1.12.0 h1:7tESER0uxaqsuGMv3yP3pK1drfBUXM6apG4H7/3+IgE=
github.com/donseba/go-htmx v1.12.0/go.mod h1:8PTAYvNKf8+QYis+DpAsggKz+sa2qljtMgvdAeNBh5s=
github.com/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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emersion/go-imap/v2 v2.0.0-beta.5 h1:H3858DNmBuXyMK1++YrQIRdpKE1MwBc+ywBtg3n+0wA=
github.com/emersion/go-imap/v2 v2.0.0-beta.5/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk=
github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/eritikass/githubmarkdownconvertergo v0.1.10 h1:mL93ADvYMOeT15DcGtK9AaFFc+RcWcy6kQBC6yS/5f4=
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/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE=
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-telegram/bot v1.14.2 h1:j9hXerxTuvkw7yFi3sF5jjRVGozNVKkMQSKjMeBJ5FY=
github.com/go-telegram/bot v1.14.2/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
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-telegram/bot v1.17.0 h1:Hs0kGxSj97QFqOQP0zxduY/4tSx8QDzvNI9uVRS+zmY=
github.com/go-telegram/bot v1.17.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
github.com/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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs=
github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
github.com/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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
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/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/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/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/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.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/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE=
@@ -107,98 +207,190 @@ 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/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
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-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
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/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI=
github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jolestar/go-commons-pool/v2 v2.1.2 h1:E+XGo58F23t7HtZiC/W6jzO2Ux2IccSH/yx4nD+J1CM=
github.com/jolestar/go-commons-pool/v2 v2.1.2/go.mod h1:r4NYccrkS5UqP1YQI1COyTZ9UjPJAAGTUxzcsK1kqhY=
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
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/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
github.com/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/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klippa-app/go-pdfium v1.19.2 h1:Gc/OT7wVO7xStNlDR5o/Qz0T/tsVtODsh7I1vOJXIKU=
github.com/klippa-app/go-pdfium v1.19.2/go.mod h1:X+AMQDw/TXTsgiY2vEGA7oYlQTmjyqmlt6pm6aoIDa0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
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.9.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0=
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modelcontextprotocol/go-sdk v1.2.0 h1:Y23co09300CEk8iZ/tMxIX1dVmKZkzoSBZOpJwUnc/s=
github.com/modelcontextprotocol/go-sdk v1.2.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/mudler/cogito v0.9.5-0.20260315222927-63abdec7189b h1:A74T2Lauvg61KodYqsjTYDY05kPLcW+efVZjd23dghU=
github.com/mudler/cogito v0.9.5-0.20260315222927-63abdec7189b/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
github.com/mudler/localrecall v0.6.3-0.20260618142827-d0073dd5dc32 h1:RP4BVGTHHpJIrGAwqRD3Wq1wmURmc1SxhwacnIWgI+g=
github.com/mudler/localrecall v0.6.3-0.20260618142827-d0073dd5dc32/go.mod h1:28k5n19raUrkuwXkacdNsBlj8yuSnGhpT16tu+2+4dU=
github.com/mudler/skillserver v0.0.5-0.20260221145827-0639a82c8f49 h1:dAF1ALXqqapRZo80x56BIBBcPrPbRNerbd66rdyO8J4=
github.com/mudler/skillserver v0.0.5-0.20260221145827-0639a82c8f49/go.mod h1:z3yFhcL9bSykmmh6xgGu0hyoItd4CnxgtWMEWw8uFJU=
github.com/mudler/xlog v0.0.5 h1:2unBuVC5rNGhCC86UaA94TElWFml80NL5XLK+kAmNuU=
github.com/mudler/xlog v0.0.5/go.mod h1:39f5vcd05Qd6GWKM8IjyHNQ7AmOx3ZM0YfhfIGhC18U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/oxffaa/gopher-parse-sitemap v0.0.0-20191021113419-005d2eb1def4 h1:2vmb32OdDhjZf2ETGDlr9n8RYXx7c+jXPxMiPbwnA+8=
github.com/oxffaa/gopher-parse-sitemap v0.0.0-20191021113419-005d2eb1def4/go.mod h1:2JQx4jDHmWrbABvpOayg/+OTU6ehN0IyK2EHzceXpJo=
github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY=
github.com/philippgille/chromem-go v0.7.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/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/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sashabaranov/go-openai v1.38.1 h1:TtZabbFQZa1nEni/IhVtDF/WQjVqDgd+cWR5OeddzF8=
github.com/sashabaranov/go-openai v1.38.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/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=
github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 h1:l/T7dYuJEQZOwVOpjIXr1180aM9PZL/d1MnMVIxefX4=
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64/go.mod h1:Q1NAJOuRdQCqN/VIWdnaaEhV8LpeO2rtlBP7/iDJNII=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -211,143 +403,169 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA=
github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/tmc/langchaingo v0.1.14 h1:o1qWBPigAIuFvrG6cjTFo0cZPFEZ47ZqpOYMjM15yZc=
github.com/tmc/langchaingo v0.1.14/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378=
github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw=
github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuHgTO4FXCvc=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
go.mau.fi/util v0.3.0 h1:Lt3lbRXP6ZBqTINK0EieRWor3zEwwwrDT14Z5N8RUCs=
go.mau.fi/util v0.3.0/go.mod h1:9dGsBCCbZJstx16YgnVMVi3O2bOizELoKpugLD4FoGs=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.starlark.net v0.0.0-20250417143717-f57e51f710eb h1:zOg9DxxrorEmgGUr5UPdCEwKqiqG0MlZciuCuA3XiDE=
go.starlark.net v0.0.0-20250417143717-f57e51f710eb/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
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/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
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/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
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/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
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-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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/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-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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.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.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
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.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
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.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.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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.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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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.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.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
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.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.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
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-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/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/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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/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/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+2 -85
View File
@@ -1,92 +1,9 @@
package main
import (
"log"
"os"
"path/filepath"
"strings"
"github.com/mudler/LocalAGI/core/state"
"github.com/mudler/LocalAGI/services"
"github.com/mudler/LocalAGI/webui"
"github.com/mudler/LocalAGI/cmd"
)
var baseModel = os.Getenv("LOCALAGI_MODEL")
var multimodalModel = os.Getenv("LOCALAGI_MULTIMODAL_MODEL")
var apiURL = os.Getenv("LOCALAGI_LLM_API_URL")
var apiKey = os.Getenv("LOCALAGI_LLM_API_KEY")
var timeout = os.Getenv("LOCALAGI_TIMEOUT")
var stateDir = os.Getenv("LOCALAGI_STATE_DIR")
var localRAG = os.Getenv("LOCALAGI_LOCALRAG_URL")
var withLogs = os.Getenv("LOCALAGI_ENABLE_CONVERSATIONS_LOGGING") == "true"
var apiKeysEnv = os.Getenv("LOCALAGI_API_KEYS")
var imageModel = os.Getenv("LOCALAGI_IMAGE_MODEL")
var conversationDuration = os.Getenv("LOCALAGI_CONVERSATION_DURATION")
func init() {
if baseModel == "" {
panic("LOCALAGI_MODEL not set")
}
if apiURL == "" {
panic("LOCALAGI_API_URL not set")
}
if timeout == "" {
timeout = "5m"
}
if stateDir == "" {
cwd, err := os.Getwd()
if err != nil {
panic(err)
}
stateDir = filepath.Join(cwd, "pool")
}
}
func main() {
// make sure state dir exists
os.MkdirAll(stateDir, 0755)
apiKeys := []string{}
if apiKeysEnv != "" {
apiKeys = strings.Split(apiKeysEnv, ",")
}
// Create the agent pool
pool, err := state.NewAgentPool(
baseModel,
multimodalModel,
imageModel,
apiURL,
apiKey,
stateDir,
localRAG,
services.Actions,
services.Connectors,
services.DynamicPrompts,
timeout,
withLogs,
)
if err != nil {
panic(err)
}
// Create the application
app := webui.NewApp(
webui.WithPool(pool),
webui.WithConversationStoreduration(conversationDuration),
webui.WithApiKeys(apiKeys...),
webui.WithLLMAPIUrl(apiURL),
webui.WithLLMAPIKey(apiKey),
webui.WithLLMModel(baseModel),
webui.WithStateDir(stateDir),
)
// Start the agents
if err := pool.StartAll(); err != nil {
panic(err)
}
// Start the web server
log.Fatal(app.Listen(":3000"))
cmd.Execute()
}
+7 -7
View File
@@ -8,13 +8,13 @@ import (
// AgentConfig represents the configuration for an agent
type AgentConfig struct {
Name string `json:"name"`
Actions []string `json:"actions,omitempty"`
Connectors []string `json:"connectors,omitempty"`
PromptBlocks []string `json:"prompt_blocks,omitempty"`
InitialPrompt string `json:"initial_prompt,omitempty"`
Parallel bool `json:"parallel,omitempty"`
Config map[string]interface{} `json:"config,omitempty"`
Name string `json:"name"`
Actions []string `json:"actions,omitempty"`
Connectors []string `json:"connectors,omitempty"`
PromptBlocks []string `json:"prompt_blocks,omitempty"`
InitialPrompt string `json:"initial_prompt,omitempty"`
Parallel bool `json:"parallel,omitempty"`
EnableReasoning bool `json:"enable_reasoning,omitempty"`
}
// AgentStatus represents the status of an agent
+94 -6
View File
@@ -4,18 +4,58 @@ import (
"encoding/json"
"fmt"
"net/http"
"github.com/sashabaranov/go-openai/jsonschema"
)
// UserLocation represents the user's location for web search
type UserLocation struct {
Type string `json:"type"`
City *string `json:"city,omitempty"`
Country *string `json:"country,omitempty"`
Region *string `json:"region,omitempty"`
Timezone *string `json:"timezone,omitempty"`
}
type Tool struct {
Type string `json:"type"`
// Function tool fields (used when type == "function")
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Parameters *jsonschema.Definition `json:"parameters,omitempty"`
// Web search tool fields (used when type == "web_search_preview" etc.)
SearchContextSize *string `json:"search_context_size,omitempty"`
UserLocation *UserLocation `json:"user_location,omitempty"`
}
type ToolChoice struct {
Name string `json:"name"`
Type string `json:"type"`
}
// RequestBody represents the message request to the AI model
type RequestBody struct {
Model string `json:"model"`
Input any `json:"input"`
Temperature *float64 `json:"temperature,omitempty"`
Tools []Tool `json:"tools,omitempty"`
ToolChoice *ToolChoice `json:"tool_choice"`
MaxTokens *int `json:"max_output_tokens,omitempty"`
}
type InputFunctionToolCallOutput struct {
CallID string `json:"call_id"`
Output string `json:"output"`
Type string `json:"type"`
ID string `json:"id"`
Status string `json:"status"`
}
// InputMessage represents a user input message
type InputMessage struct {
Type string `json:"type"`
Role string `json:"role"`
Content any `json:"content"`
}
@@ -29,10 +69,49 @@ type ContentItem struct {
// ResponseBody represents the response from the AI model
type ResponseBody struct {
CreatedAt int64 `json:"created_at"`
Status string `json:"status"`
Error any `json:"error,omitempty"`
Output []ResponseMessage `json:"output"`
CreatedAt int64 `json:"created_at"`
Status string `json:"status"`
Error any `json:"error,omitempty"`
Output []ResponseBase `json:"output"`
Tools []Tool `json:"tools"`
}
type ResponseType string
const (
ResponseTypeFunctionToolCall ResponseType = "function_call"
ResponseTypeMessage ResponseType = "message"
)
type ResponseBase json.RawMessage
func (r *ResponseBase) UnmarshalJSON(data []byte) error {
return (*json.RawMessage)(r).UnmarshalJSON(data)
}
func (r *ResponseBase) ToMessage() (msg ResponseMessage, err error) {
err = json.Unmarshal(*r, &msg)
if msg.Type != string(ResponseTypeMessage) {
return ResponseMessage{}, fmt.Errorf("Expected %s, not %s", ResponseTypeMessage, msg.Type)
}
return
}
func (r *ResponseBase) ToFunctionToolCall() (msg ResponseFunctionToolCall, err error) {
err = json.Unmarshal(*r, &msg)
if msg.Type != string(ResponseTypeFunctionToolCall) {
return ResponseFunctionToolCall{}, fmt.Errorf("Expected %s, not %s", ResponseTypeFunctionToolCall, msg.Type)
}
return
}
type ResponseFunctionToolCall struct {
Arguments string `json:"arguments"`
CallID string `json:"call_id"`
Name string `json:"name"`
Type string `json:"type"`
ID string `json:"id"`
Status string `json:"status"`
}
// ResponseMessage represents a message in the response
@@ -85,7 +164,11 @@ func (c *Client) SimpleAIResponse(agentName, input string) (string, error) {
}
// Extract the text response from the output
for _, msg := range response.Output {
for _, out := range response.Output {
msg, err := out.ToMessage()
if err != nil {
return "", fmt.Errorf("out.ToMessage: %w", err)
}
if msg.Role == "assistant" {
for _, content := range msg.Content {
if content.Type == "output_text" {
@@ -113,7 +196,12 @@ func (c *Client) ChatAIResponse(agentName string, messages []InputMessage) (stri
}
// Extract the text response from the output
for _, msg := range response.Output {
for _, out := range response.Output {
msg, err := out.ToMessage()
if err != nil {
return "", fmt.Errorf("out.ToMessage: %w", err)
}
if msg.Role == "assistant" {
for _, content := range msg.Content {
if content.Type == "output_text" {
+13 -9
View File
@@ -5,21 +5,25 @@ import (
"encoding/json"
"fmt"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/xlog"
"github.com/sashabaranov/go-openai"
"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 *openai.Client, 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 *openai.Client, conv []openai.ChatCompletionMessage, model string, i jsonschema.Definition, dst any) error {
toolName := "json"
decision := openai.ChatCompletionRequest{
Model: model,
Messages: []openai.ChatCompletionMessage{
{
Role: "user",
Content: guidance,
},
},
Model: model,
Messages: conv,
Tools: []openai.Tool{
{
+303 -59
View File
@@ -11,12 +11,14 @@ import (
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/xlog"
)
var _ agent.RAGDB = &WrappedClient{}
@@ -26,7 +28,8 @@ type WrappedClient struct {
collection string
}
func NewWrappedClient(baseURL, apiKey, collection string) *WrappedClient {
func NewWrappedClient(baseURL, apiKey, c string) *WrappedClient {
collection := strings.TrimSpace(strings.ToLower(c))
wc := &WrappedClient{
Client: NewClient(baseURL, apiKey),
collection: collection,
@@ -37,6 +40,11 @@ func NewWrappedClient(baseURL, apiKey, collection string) *WrappedClient {
return wc
}
// Collection returns the collection name for this client.
func (c *WrappedClient) Collection() string {
return c.collection
}
func (c *WrappedClient) Count() int {
entries, err := c.ListEntries(c.collection)
if err != nil {
@@ -85,7 +93,39 @@ func (c *WrappedClient) Store(s string) error {
}
defer os.Remove(f)
return c.Client.Store(c.collection, f)
_, err = c.Client.Store(c.collection, f)
return err
}
// GetEntryContent returns the full file content (no chunk overlap) and the number of chunks for the entry.
func (c *WrappedClient) GetEntryContent(entry string) (content string, chunkCount int, err error) {
return c.Client.GetEntryContent(c.collection, entry)
}
// apiResponse is the standardized LocalRecall API response wrapper (since 3f73ff3a).
type apiResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
Error *apiError `json:"error,omitempty"`
}
type apiError struct {
Code string `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
}
// parseAPIError reads the response body and returns an error from the API response or a generic message.
func parseAPIError(resp *http.Response, body []byte, fallback string) error {
var wrap apiResponse
if err := json.Unmarshal(body, &wrap); err == nil && wrap.Error != nil {
if wrap.Error.Details != "" {
return fmt.Errorf("%s: %s", wrap.Error.Message, wrap.Error.Details)
}
return errors.New(wrap.Error.Message)
}
return fmt.Errorf("%s: %s", fallback, string(body))
}
// Result represents a single result from a query.
@@ -101,6 +141,13 @@ type Result struct {
Similarity float32
}
// EntryChunk represents a single chunk (legacy; GetEntryContent now returns full file content).
type EntryChunk struct {
ID string `json:"id"`
Content string `json:"content"`
Metadata map[string]string `json:"metadata"`
}
// Client is a client for the RAG API
type Client struct {
BaseURL string
@@ -151,7 +198,8 @@ func (c *Client) CreateCollection(name string) error {
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return errors.New("failed to create collection")
body, _ := io.ReadAll(resp.Body)
return parseAPIError(resp, body, "failed to create collection")
}
return nil
@@ -174,17 +222,30 @@ func (c *Client) ListCollections() ([]string, error) {
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("failed to list collections")
}
var collections []string
err = json.NewDecoder(resp.Body).Decode(&collections)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return collections, nil
if resp.StatusCode != http.StatusOK {
return nil, parseAPIError(resp, body, "failed to list collections")
}
var wrap apiResponse
if err := json.Unmarshal(body, &wrap); err != nil || !wrap.Success {
if wrap.Error != nil {
return nil, errors.New(wrap.Error.Message)
}
return nil, fmt.Errorf("invalid response: %w", err)
}
var data struct {
Collections []string `json:"collections"`
}
if err := json.Unmarshal(wrap.Data, &data); err != nil {
return nil, err
}
return data.Collections, nil
}
// ListEntries lists all entries in a collection
@@ -204,17 +265,79 @@ func (c *Client) ListEntries(collection string) ([]string, error) {
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("failed to list entries")
}
var entries []string
err = json.NewDecoder(resp.Body).Decode(&entries)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return entries, nil
if resp.StatusCode != http.StatusOK {
return nil, parseAPIError(resp, body, "failed to list entries")
}
var wrap apiResponse
if err := json.Unmarshal(body, &wrap); err != nil || !wrap.Success {
if wrap.Error != nil {
return nil, errors.New(wrap.Error.Message)
}
return nil, fmt.Errorf("invalid response: %w", err)
}
var data struct {
Entries []string `json:"entries"`
Keys []string `json:"keys"`
}
if err := json.Unmarshal(wrap.Data, &data); err != nil {
return nil, err
}
if len(data.Keys) > 0 {
return data.Keys, nil
}
return data.Entries, nil
}
// GetEntryContent returns the full file content (no chunk overlap) and the number of chunks for the entry.
func (c *Client) GetEntryContent(collection, entry string) (content string, chunkCount int, err error) {
entryEscaped := url.PathEscape(entry)
reqURL := fmt.Sprintf("%s/api/collections/%s/entries/%s", c.BaseURL, collection, entryEscaped)
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
if err != nil {
return "", 0, err
}
c.addAuthHeader(req)
httpClient := &http.Client{}
resp, err := httpClient.Do(req)
if err != nil {
return "", 0, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", 0, err
}
if resp.StatusCode != http.StatusOK {
return "", 0, parseAPIError(resp, body, "failed to get entry content")
}
var wrap apiResponse
if err := json.Unmarshal(body, &wrap); err != nil || !wrap.Success {
if wrap.Error != nil {
return "", 0, errors.New(wrap.Error.Message)
}
return "", 0, fmt.Errorf("invalid response: %w", err)
}
var data struct {
Content string `json:"content"`
ChunkCount int `json:"chunk_count"`
}
if err := json.Unmarshal(wrap.Data, &data); err != nil {
return "", 0, err
}
return data.Content, data.ChunkCount, nil
}
// DeleteEntry deletes an entry in a collection
@@ -244,19 +367,30 @@ func (c *Client) DeleteEntry(collection, entry string) ([]string, error) {
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyResult := new(bytes.Buffer)
bodyResult.ReadFrom(resp.Body)
return nil, errors.New("failed to delete entry: " + bodyResult.String())
}
var results []string
err = json.NewDecoder(resp.Body).Decode(&results)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return results, nil
if resp.StatusCode != http.StatusOK {
return nil, parseAPIError(resp, body, "failed to delete entry")
}
var wrap apiResponse
if err := json.Unmarshal(body, &wrap); err != nil || !wrap.Success {
if wrap.Error != nil {
return nil, errors.New(wrap.Error.Message)
}
return nil, fmt.Errorf("invalid response: %w", err)
}
var data struct {
RemainingEntries []string `json:"remaining_entries"`
}
if err := json.Unmarshal(wrap.Data, &data); err != nil {
return nil, err
}
return data.RemainingEntries, nil
}
// Search searches a collection
@@ -287,17 +421,30 @@ func (c *Client) Search(collection, query string, maxResults int) ([]Result, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("failed to search collection")
}
var results []Result
err = json.NewDecoder(resp.Body).Decode(&results)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return results, nil
if resp.StatusCode != http.StatusOK {
return nil, parseAPIError(resp, body, "failed to search collection")
}
var wrap apiResponse
if err := json.Unmarshal(body, &wrap); err != nil || !wrap.Success {
if wrap.Error != nil {
return nil, errors.New(wrap.Error.Message)
}
return nil, fmt.Errorf("invalid response: %w", err)
}
var data struct {
Results []Result `json:"results"`
}
if err := json.Unmarshal(wrap.Data, &data); err != nil {
return nil, err
}
return data.Results, nil
}
// Reset resets a collection
@@ -318,21 +465,20 @@ func (c *Client) Reset(collection string) error {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b := new(bytes.Buffer)
b.ReadFrom(resp.Body)
return errors.New("failed to reset collection: " + b.String())
body, _ := io.ReadAll(resp.Body)
return parseAPIError(resp, body, "failed to reset collection")
}
return nil
}
// Store uploads a file to a collection
func (c *Client) Store(collection, filePath string) error {
// Store uploads a file to a collection and returns the assigned entry key.
func (c *Client) Store(collection, filePath string) (string, error) {
url := fmt.Sprintf("%s/api/collections/%s/upload", c.BaseURL, collection)
file, err := os.Open(filePath)
if err != nil {
return err
return "", err
}
defer file.Close()
@@ -341,22 +487,22 @@ func (c *Client) Store(collection, filePath string) error {
part, err := writer.CreateFormFile("file", file.Name())
if err != nil {
return err
return "", err
}
_, err = io.Copy(part, file)
if err != nil {
return err
return "", err
}
err = writer.Close()
if err != nil {
return err
return "", err
}
req, err := http.NewRequest(http.MethodPost, url, body)
if err != nil {
return err
return "", err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
c.addAuthHeader(req)
@@ -364,26 +510,124 @@ func (c *Client) Store(collection, filePath string) error {
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b := new(bytes.Buffer)
b.ReadFrom(resp.Body)
type response struct {
Error string `json:"error"`
}
var r response
err = json.Unmarshal(b.Bytes(), &r)
if err == nil {
return errors.New("failed to upload file: " + r.Error)
}
return errors.New("failed to upload file")
body, _ := io.ReadAll(resp.Body)
return "", parseAPIError(resp, body, "failed to upload file")
}
var result struct {
Status string `json:"status"`
Filename string `json:"filename"`
Key string `json:"key"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", nil // upload succeeded, can't parse key
}
return result.Key, nil
}
// SourceInfo represents an external source for a collection (LocalRecall API contract).
type SourceInfo struct {
URL string `json:"url"`
UpdateInterval int `json:"update_interval"` // minutes
LastUpdate string `json:"last_update"` // RFC3339
}
// AddSource registers an external source for a collection.
func (c *Client) AddSource(collection, url string, updateIntervalMinutes int) error {
reqURL := fmt.Sprintf("%s/api/collections/%s/sources", c.BaseURL, collection)
var body struct {
URL string `json:"url"`
UpdateInterval int `json:"update_interval"`
}
body.URL = url
body.UpdateInterval = updateIntervalMinutes
if body.UpdateInterval < 1 {
body.UpdateInterval = 60
}
payload, err := json.Marshal(body)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewBuffer(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
c.addAuthHeader(req)
resp, err := (&http.Client{}).Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return parseAPIError(resp, b, "failed to add source")
}
return nil
}
// RemoveSource removes an external source from a collection.
func (c *Client) RemoveSource(collection, url string) error {
reqURL := fmt.Sprintf("%s/api/collections/%s/sources", c.BaseURL, collection)
payload, err := json.Marshal(map[string]string{"url": url})
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodDelete, reqURL, bytes.NewBuffer(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
c.addAuthHeader(req)
resp, err := (&http.Client{}).Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return parseAPIError(resp, b, "failed to remove source")
}
return nil
}
// ListSources returns external sources for a collection.
func (c *Client) ListSources(collection string) ([]SourceInfo, error) {
reqURL := fmt.Sprintf("%s/api/collections/%s/sources", c.BaseURL, collection)
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
if err != nil {
return nil, err
}
c.addAuthHeader(req)
resp, err := (&http.Client{}).Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, parseAPIError(resp, body, "failed to list sources")
}
var wrap apiResponse
if err := json.Unmarshal(body, &wrap); err != nil || !wrap.Success {
if wrap.Error != nil {
return nil, errors.New(wrap.Error.Message)
}
return nil, fmt.Errorf("invalid response: %w", err)
}
var data struct {
Sources []SourceInfo `json:"sources"`
}
if err := json.Unmarshal(wrap.Data, &data); err != nil {
return nil, err
}
return data.Sources, nil
}
+5
View File
@@ -0,0 +1,5 @@
package ptr
func To[T any](v T) *T {
return &v
}
-71
View File
@@ -1,71 +0,0 @@
package xlog
import (
"context"
"log/slog"
"os"
"runtime"
)
var logger *slog.Logger
func init() {
var level = slog.LevelDebug
switch os.Getenv("LOG_LEVEL") {
case "info":
level = slog.LevelInfo
case "warn":
level = slog.LevelWarn
case "error":
level = slog.LevelError
case "debug":
level = slog.LevelDebug
}
var opts = &slog.HandlerOptions{
Level: level,
}
var handler slog.Handler
if os.Getenv("LOG_FORMAT") == "json" {
handler = slog.NewJSONHandler(os.Stdout, opts)
} else {
handler = slog.NewTextHandler(os.Stdout, opts)
}
logger = slog.New(handler)
}
func _log(level slog.Level, msg string, args ...any) {
_, f, l, _ := runtime.Caller(2)
group := slog.Group(
"source",
slog.Attr{
Key: "file",
Value: slog.AnyValue(f),
},
slog.Attr{
Key: "L",
Value: slog.AnyValue(l),
},
)
args = append(args, group)
logger.Log(context.Background(), level, msg, args...)
}
func Info(msg string, args ...any) {
_log(slog.LevelInfo, msg, args...)
}
func Debug(msg string, args ...any) {
_log(slog.LevelDebug, msg, args...)
}
func Error(msg string, args ...any) {
_log(slog.LevelError, msg, args...)
}
func Warn(msg string, args ...any) {
_log(slog.LevelWarn, msg, args...)
}
+401 -148
View File
@@ -4,12 +4,16 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"github.com/mudler/LocalAGI/core/action"
"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/xlog"
"github.com/mudler/LocalAGI/services/actions"
)
@@ -20,6 +24,7 @@ const (
ActionCustom = "custom"
ActionGithubIssueLabeler = "github-issue-labeler"
ActionGithubIssueOpener = "github-issue-opener"
ActionGithubIssueEditor = "github-issue-editor"
ActionGithubIssueCloser = "github-issue-closer"
ActionGithubIssueSearcher = "github-issue-searcher"
ActionGithubRepositoryGet = "github-repository-get-content"
@@ -32,15 +37,37 @@ const (
ActionGithubPRCreator = "github-pr-creator"
ActionGithubGetAllContent = "github-get-all-repository-content"
ActionGithubREADME = "github-readme"
ActionGithubRepositorySearchFiles = "github-repository-search-files"
ActionGithubRepositoryListFiles = "github-repository-list-files"
ActionScraper = "scraper"
ActionWikipedia = "wikipedia"
ActionBrowse = "browse"
ActionTwitterPost = "twitter-post"
ActionSendMail = "send-mail"
ActionGenerateImage = "generate_image"
ActionGenerateSong = "generate_song"
ActionGeneratePDF = "generate_pdf"
ActionCounter = "counter"
ActionCallAgents = "call_agents"
ActionShellcommand = "shell-command"
ActionSendTelegramMessage = "send-telegram-message"
ActionSetReminder = "set_reminder"
ActionSetRecurringReminder = "set_recurring_reminder"
ActionSetOneTimeReminder = "set_onetime_reminder"
ActionListReminders = "list_reminders"
ActionRemoveReminder = "remove_reminder"
ActionAddToMemory = "add_to_memory"
ActionListMemory = "list_memory"
ActionRemoveFromMemory = "remove_from_memory"
ActionSearchMemory = "search_memory"
ActionPiKVMPowerControl = "pikvm_power_control"
ActionWebhook = "webhook"
)
const (
nameField = "name"
descriptionField = "description"
configurationField = "configuration"
)
var AvailableActions = []string{
@@ -48,10 +75,13 @@ var AvailableActions = []string{
ActionCustom,
ActionGithubIssueLabeler,
ActionGithubIssueOpener,
ActionGithubIssueEditor,
ActionGithubIssueCloser,
ActionGithubIssueSearcher,
ActionGithubRepositoryGet,
ActionGithubGetAllContent,
ActionGithubRepositorySearchFiles,
ActionGithubRepositoryListFiles,
ActionGithubRepositoryCreateOrUpdate,
ActionGithubIssueReader,
ActionGithubIssueCommenter,
@@ -65,51 +95,342 @@ var AvailableActions = []string{
ActionWikipedia,
ActionSendMail,
ActionGenerateImage,
ActionGenerateSong,
ActionGeneratePDF,
ActionTwitterPost,
ActionCounter,
ActionCallAgents,
ActionShellcommand,
ActionSendTelegramMessage,
ActionSetReminder,
ActionListReminders,
ActionRemoveReminder,
ActionAddToMemory,
ActionListMemory,
ActionRemoveFromMemory,
ActionSearchMemory,
ActionPiKVMPowerControl,
ActionWebhook,
}
func Actions(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
return func(ctx context.Context, pool *state.AgentPool) []types.Action {
allActions := []types.Action{}
var DefaultActions = []config.FieldGroup{
{
Name: "search",
Label: "Search",
Fields: actions.SearchConfigMeta(),
},
{
Name: "generate_image",
Label: "Generate Image",
Fields: actions.GenImageConfigMeta(),
},
{
Name: "generate_song",
Label: "Generate Song",
Fields: actions.GenSongConfigMeta(),
},
{
Name: "generate_pdf",
Label: "Generate PDF",
Fields: actions.GenPDFConfigMeta(),
},
{
Name: "add_to_memory",
Label: "Add to Memory",
Fields: actions.AddToMemoryConfigMeta(),
},
{
Name: "list_memory",
Label: "List Memory",
Fields: actions.ListMemoryConfigMeta(),
},
{
Name: "remove_from_memory",
Label: "Remove from Memory",
Fields: actions.RemoveFromMemoryConfigMeta(),
},
{
Name: "search_memory",
Label: "Search Memory",
Fields: actions.SearchMemoryConfigMeta(),
},
{
Name: "github-issue-labeler",
Label: "GitHub Issue Labeler",
Fields: actions.GithubIssueLabelerConfigMeta(),
},
{
Name: "github-issue-opener",
Label: "GitHub Issue Opener",
Fields: actions.GithubIssueOpenerConfigMeta(),
},
{
Name: "github-issue-editor",
Label: "GitHub Issue Editor",
Fields: actions.GithubIssueEditorConfigMeta(),
},
{
Name: "github-issue-closer",
Label: "GitHub Issue Closer",
Fields: actions.GithubIssueCloserConfigMeta(),
},
{
Name: "github-issue-commenter",
Label: "GitHub Issue Commenter",
Fields: actions.GithubIssueCommenterConfigMeta(),
},
{
Name: "github-issue-reader",
Label: "GitHub Issue Reader",
Fields: actions.GithubIssueReaderConfigMeta(),
},
{
Name: "github-issue-searcher",
Label: "GitHub Issue Search",
Fields: actions.GithubIssueSearchConfigMeta(),
},
{
Name: "github-repository-get-content",
Label: "GitHub Repository Get Content",
Fields: actions.GithubRepositoryGetContentConfigMeta(),
},
{
Name: "github-get-all-repository-content",
Label: "GitHub Get All Repository Content",
Fields: actions.GithubRepositoryGetAllContentConfigMeta(),
},
{
Name: "github-repository-search-files",
Label: "GitHub Repository Search Files",
Fields: actions.GithubRepositorySearchFilesConfigMeta(),
},
{
Name: "github-repository-list-files",
Label: "GitHub Repository List Files",
Fields: actions.GithubRepositoryListFilesConfigMeta(),
},
{
Name: "github-repository-create-or-update-content",
Label: "GitHub Repository Create/Update Content",
Fields: actions.GithubRepositoryCreateOrUpdateContentConfigMeta(),
},
{
Name: "github-readme",
Label: "GitHub Repository README",
Fields: actions.GithubRepositoryREADMEConfigMeta(),
},
{
Name: "github-pr-reader",
Label: "GitHub PR Reader",
Fields: actions.GithubPRReaderConfigMeta(),
},
{
Name: "github-pr-commenter",
Label: "GitHub PR Commenter",
Fields: actions.GithubPRCommenterConfigMeta(),
},
{
Name: "github-pr-reviewer",
Label: "GitHub PR Reviewer",
Fields: actions.GithubPRReviewerConfigMeta(),
},
{
Name: "github-pr-creator",
Label: "GitHub PR Creator",
Fields: actions.GithubPRCreatorConfigMeta(),
},
{
Name: "twitter-post",
Label: "Twitter Post",
Fields: actions.TwitterPostConfigMeta(),
},
{
Name: "send-mail",
Label: "Send Mail",
Fields: actions.SendMailConfigMeta(),
},
{
Name: "shell-command",
Label: "Shell Command",
Fields: actions.ShellConfigMeta(),
},
{
Name: "custom",
Label: "Custom",
Fields: action.CustomConfigMeta(),
},
{
Name: "scraper",
Label: "Scraper",
Fields: []config.Field{},
},
{
Name: "wikipedia",
Label: "Wikipedia",
Fields: []config.Field{},
},
{
Name: "browse",
Label: "Browse",
Fields: []config.Field{},
},
{
Name: "counter",
Label: "Counter",
Fields: []config.Field{},
},
{
Name: "call_agents",
Label: "Call Agents",
Fields: actions.CallAgentConfigMeta(),
},
{
Name: "send-telegram-message",
Label: "Send Telegram Message",
Fields: actions.SendTelegramMessageConfigMeta(),
},
{
Name: "set_recurring_reminder",
Label: "Set Recurring Reminder",
Fields: []config.Field{},
},
{
Name: "set_onetime_reminder",
Label: "Set One-Time Reminder",
Fields: []config.Field{},
},
{
Name: "list_reminders",
Label: "List Reminders",
Fields: []config.Field{},
},
{
Name: "remove_reminder",
Label: "Remove Reminder",
Fields: []config.Field{},
},
{
Name: "pikvm_power_control",
Label: "PiKVM Power Control",
Fields: actions.PiKVMConfigMeta(),
},
{
Name: "webhook",
Label: "Webhook",
Fields: actions.WebhookConfigMeta(),
},
}
agentName := a.Name
for _, a := range a.Actions {
var config map[string]string
if err := json.Unmarshal([]byte(a.Config), &config); err != nil {
xlog.Error("Error unmarshalling action config", "error", err)
continue
}
a, err := Action(a.Name, agentName, config, pool)
if err != nil {
continue
}
allActions = append(allActions, a)
}
const (
ActionConfigSSHBoxURL = "sshbox-url"
ConfigStateDir = "state-dir"
CustomActionsDir = "custom-actions-dir"
)
func customActions(customActionsDir string, existingActionConfigs map[string]map[string]string) (allActions []types.Action) {
files, err := os.ReadDir(customActionsDir)
if err != nil {
xlog.Error("Error reading custom actions directory", "error", err)
return allActions
}
for _, file := range files {
if filepath.Ext(file.Name()) != ".go" {
continue
}
content, err := os.ReadFile(filepath.Join(customActionsDir, file.Name()))
if err != nil {
xlog.Error("Error reading custom action file", "error", err, "file", file.Name())
continue
}
actionName := strings.TrimSuffix(file.Name(), ".go")
actionConfig := map[string]string{
"name": actionName,
"description": "",
"code": string(content),
"unsafe": "false",
}
if c, exists := existingActionConfigs[actionName]; exists {
// We allow the user to customize name and description
actionConfig[descriptionField] = c[descriptionField]
actionConfig[nameField] = c[nameField]
actionConfig[configurationField] = c[configurationField]
}
a, err := Action(ActionCustom, "", actionConfig, nil, map[string]string{})
if err != nil {
xlog.Error("Error creating custom action", "error", err, "file", file.Name())
continue
}
allActions = append(allActions, a)
}
return
}
func Action(name, agentName string, config map[string]string, pool *state.AgentPool) (types.Action, error) {
func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
return func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
return func(ctx context.Context, pool *state.AgentPool) []types.Action {
allActions := []types.Action{}
agentName := a.Name
existingActionConfigs := map[string]map[string]string{}
for _, a := range a.Actions {
var config map[string]string
if err := json.Unmarshal([]byte(a.Config), &config); err != nil {
xlog.Error("Error unmarshalling action config", "error", err)
continue
}
existingActionConfigs[a.Name] = config
a, err := Action(a.Name, agentName, config, pool, actionsConfigs)
if err != nil {
continue
}
allActions = append(allActions, a)
}
// Now we will scan a directory for custom actions
if actionsConfigs[CustomActionsDir] != "" {
allActions = append(allActions, customActions(actionsConfigs[CustomActionsDir], existingActionConfigs)...)
}
return allActions
}
}
}
func Action(name, agentName string, config map[string]string, pool *state.AgentPool, actionsConfigs map[string]string) (types.Action, error) {
var a types.Action
var err error
if config == nil {
config = map[string]string{}
}
memoryIdxPath := memoryIndexPath(agentName, actionsConfigs)
switch name {
case ActionCustom:
a, err = action.NewCustom(config, "")
case ActionGenerateImage:
a = actions.NewGenImage(config)
case ActionGenerateSong:
a = actions.NewGenSong(config)
case ActionGeneratePDF:
a = actions.NewGenPDF(config)
case ActionSearch:
a = actions.NewSearch(config)
case ActionGithubIssueLabeler:
a = actions.NewGithubIssueLabeler(config)
case ActionGithubIssueOpener:
a = actions.NewGithubIssueOpener(config)
case ActionGithubIssueEditor:
a = actions.NewGithubIssueEditor(config)
case ActionGithubIssueCloser:
a = actions.NewGithubIssueCloser(config)
case ActionGithubIssueSearcher:
@@ -126,6 +447,10 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
a = actions.NewGithubPRCreator(config)
case ActionGithubGetAllContent:
a = actions.NewGithubRepositoryGetAllContent(config)
case ActionGithubRepositorySearchFiles:
a = actions.NewGithubRepositorySearchFiles(config)
case ActionGithubRepositoryListFiles:
a = actions.NewGithubRepositoryListFiles(config)
case ActionGithubIssueCommenter:
a = actions.NewGithubIssueCommenter(config)
case ActionGithubRepositoryGet:
@@ -142,6 +467,8 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
a = actions.NewBrowse(config)
case ActionSendMail:
a = actions.NewSendMail(config)
case ActionWebhook:
a = actions.NewWebhook(config)
case ActionTwitterPost:
a = actions.NewPostTweet(config)
case ActionCounter:
@@ -149,7 +476,27 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
case ActionCallAgents:
a = actions.NewCallAgent(config, agentName, pool.InternalAPI())
case ActionShellcommand:
a = actions.NewShell(config)
a = actions.NewShell(config, actionsConfigs[ActionConfigSSHBoxURL])
case ActionSendTelegramMessage:
a = actions.NewSendTelegramMessageRunner(config)
case ActionSetRecurringReminder:
a = action.NewRecurringReminder()
case ActionSetOneTimeReminder:
a = action.NewOneTimeReminder()
case ActionListReminders:
a = action.NewListReminders()
case ActionRemoveReminder:
a = action.NewRemoveReminder()
case ActionAddToMemory:
a, _, _, _ = actions.NewMemoryActions(memoryIdxPath, config)
case ActionListMemory:
_, a, _, _ = actions.NewMemoryActions(memoryIdxPath, config)
case ActionRemoveFromMemory:
_, _, a, _ = actions.NewMemoryActions(memoryIdxPath, config)
case ActionSearchMemory:
_, _, _, a = actions.NewMemoryActions(memoryIdxPath, config)
case ActionPiKVMPowerControl:
a = actions.NewPiKVMAction(config)
default:
xlog.Error("Action not found", "name", name)
return nil, fmt.Errorf("Action not found")
@@ -162,132 +509,38 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
return a, nil
}
func ActionsConfigMeta() []config.FieldGroup {
return []config.FieldGroup{
{
Name: "search",
Label: "Search",
Fields: actions.SearchConfigMeta(),
},
{
Name: "generate_image",
Label: "Generate Image",
Fields: actions.GenImageConfigMeta(),
},
{
Name: "github-issue-labeler",
Label: "GitHub Issue Labeler",
Fields: actions.GithubIssueLabelerConfigMeta(),
},
{
Name: "github-issue-opener",
Label: "GitHub Issue Opener",
Fields: actions.GithubIssueOpenerConfigMeta(),
},
{
Name: "github-issue-closer",
Label: "GitHub Issue Closer",
Fields: actions.GithubIssueCloserConfigMeta(),
},
{
Name: "github-issue-commenter",
Label: "GitHub Issue Commenter",
Fields: actions.GithubIssueCommenterConfigMeta(),
},
{
Name: "github-issue-reader",
Label: "GitHub Issue Reader",
Fields: actions.GithubIssueReaderConfigMeta(),
},
{
Name: "github-issue-searcher",
Label: "GitHub Issue Search",
Fields: actions.GithubIssueSearchConfigMeta(),
},
{
Name: "github-repository-get-content",
Label: "GitHub Repository Get Content",
Fields: actions.GithubRepositoryGetContentConfigMeta(),
},
{
Name: "github-get-all-repository-content",
Label: "GitHub Get All Repository Content",
Fields: actions.GithubRepositoryGetAllContentConfigMeta(),
},
{
Name: "github-repository-create-or-update-content",
Label: "GitHub Repository Create/Update Content",
Fields: actions.GithubRepositoryCreateOrUpdateContentConfigMeta(),
},
{
Name: "github-readme",
Label: "GitHub Repository README",
Fields: actions.GithubRepositoryREADMEConfigMeta(),
},
{
Name: "github-pr-reader",
Label: "GitHub PR Reader",
Fields: actions.GithubPRReaderConfigMeta(),
},
{
Name: "github-pr-commenter",
Label: "GitHub PR Commenter",
Fields: actions.GithubPRCommenterConfigMeta(),
},
{
Name: "github-pr-reviewer",
Label: "GitHub PR Reviewer",
Fields: actions.GithubPRReviewerConfigMeta(),
},
{
Name: "github-pr-creator",
Label: "GitHub PR Creator",
Fields: actions.GithubPRCreatorConfigMeta(),
},
{
Name: "twitter-post",
Label: "Twitter Post",
Fields: actions.TwitterPostConfigMeta(),
},
{
Name: "send-mail",
Label: "Send Mail",
Fields: actions.SendMailConfigMeta(),
},
{
Name: "shell-command",
Label: "Shell Command",
Fields: actions.ShellConfigMeta(),
},
{
Name: "custom",
Label: "Custom",
Fields: action.CustomConfigMeta(),
},
{
Name: "scraper",
Label: "Scraper",
Fields: []config.Field{},
},
{
Name: "wikipedia",
Label: "Wikipedia",
Fields: []config.Field{},
},
{
Name: "browse",
Label: "Browse",
Fields: []config.Field{},
},
{
Name: "counter",
Label: "Counter",
Fields: []config.Field{},
},
{
Name: "call_agents",
Label: "Call Agents",
Fields: []config.Field{},
},
func ActionsConfigMeta(customActionDir string) []config.FieldGroup {
all := slices.Clone(DefaultActions)
if customActionDir != "" {
actions := customActions(customActionDir, map[string]map[string]string{})
for _, a := range actions {
all = append(all, config.FieldGroup{
Name: a.Definition().Name.String(),
Label: a.Definition().Name.String(),
Fields: []config.Field{
{
Name: nameField,
Label: "Name",
Type: config.FieldTypeText,
HelpText: "Name of the custom action",
},
{
Name: descriptionField,
Label: "Description",
Type: config.FieldTypeTextarea,
HelpText: "Description of the custom action",
},
{
Name: configurationField,
Label: "Configuration",
Type: config.FieldTypeTextarea,
HelpText: "Configuration of the custom action",
},
},
})
}
}
return all
}
+1 -1
View File
@@ -18,7 +18,7 @@ func NewBrowse(config map[string]string) *BrowseAction {
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 {
URL string `json:"url"`
}{}
+73 -6
View File
@@ -3,26 +3,56 @@ package actions
import (
"context"
"fmt"
"slices"
"strings"
"github.com/mudler/LocalAGI/core/state"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/sashabaranov/go-openai"
"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 {
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{
pool: pool,
myName: agentName,
pool: pool,
myName: agentName,
whitelist: whitelist,
blacklist: blacklist,
}
}
type CallAgentAction struct {
pool *state.AgentPoolInternalAPI
myName string
pool *state.AgentPoolInternalAPI
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 {
AgentName string `json:"agent_name"`
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
}
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 {
allAgents := a.pool.AllAgents()
agents := []string{}
for _, ag := range allAgents {
if ag != a.myName {
if a.isAllowedToBeCalled(ag) {
agents = append(agents, ag)
}
}
@@ -125,3 +174,21 @@ func (a *CallAgentAction) Definition() types.ActionDefinition {
func (a *CallAgentAction) Plannable() bool {
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.",
},
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ func NewCounter(config map[string]string) *CounterAction {
}
// 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
request := struct {
Name string `json:"name"`
+1 -1
View File
@@ -29,7 +29,7 @@ type GenImageAction struct {
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 {
Prompt string `json:"prompt"`
Size string `json:"size"`
+2 -2
View File
@@ -42,7 +42,7 @@ var _ = Describe("GenImageAction", func() {
"size": "256x256",
}
url, err := action.Run(ctx, params)
url, err := action.Run(ctx, nil, params)
Expect(err).ToNot(HaveOccurred())
Expect(url).ToNot(BeEmpty())
})
@@ -52,7 +52,7 @@ var _ = Describe("GenImageAction", func() {
"size": "256x256",
}
_, err := action.Run(ctx, params)
_, err := action.Run(ctx, nil, params)
Expect(err).To(HaveOccurred())
})
})
+165
View File
@@ -0,0 +1,165 @@
package actions
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/gomarkdown/markdown/ast"
"github.com/gomarkdown/markdown/parser"
"github.com/jung-kurt/gofpdf"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/xlog"
"github.com/sashabaranov/go-openai/jsonschema"
)
const (
MetadataPDFs = "pdf_paths"
)
// NewGenPDF creates a new PDF generation action
func NewGenPDF(config map[string]string) *GenPDFAction {
a := &GenPDFAction{
outputDir: config["outputDir"],
cleanOnStart: config["cleanOnStart"] == "true" || config["cleanOnStart"] == "1",
}
if a.outputDir != "" {
if err := os.MkdirAll(a.outputDir, 0755); err != nil {
xlog.Error("Failed to create output directory", "path", a.outputDir, "error", err)
}
if a.cleanOnStart {
entries, err := os.ReadDir(a.outputDir)
if err == nil {
for _, e := range entries {
_ = os.Remove(filepath.Join(a.outputDir, e.Name()))
}
}
}
}
return a
}
type GenPDFAction struct {
outputDir string
cleanOnStart bool
}
func (a *GenPDFAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Title string `json:"title"`
Content string `json:"content"`
Filename string `json:"filename"`
}{}
if err := params.Unmarshal(&result); err != nil {
return types.ActionResult{}, err
}
if result.Content == "" {
return types.ActionResult{}, fmt.Errorf("content is required")
}
if a.outputDir == "" {
return types.ActionResult{}, fmt.Errorf("outputDir is required for generate_pdf (configure the action with an output directory)")
}
// Generate filename if not provided
filename := result.Filename
if filename == "" {
filename = fmt.Sprintf("document_%d", time.Now().UnixNano())
}
// Clean filename to prevent path traversal
filename = filepath.Base(filename)
// Ensure filename has .pdf extension
if !strings.HasSuffix(strings.ToLower(filename), ".pdf") {
filename = filename + ".pdf"
}
// Create PDF
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.AddPage()
tr := pdf.UnicodeTranslatorFromDescriptor("")
// Add title if provided
if result.Title != "" {
pdf.SetFont("Arial", "B", 16)
pdf.MultiCell(0, 10, tr(result.Title), "", "", false)
pdf.Ln(5)
}
// Add content: parse as markdown and render, or fall back to plain text
pdf.SetFont("Arial", "", 12)
p := parser.NewWithExtensions(parser.CommonExtensions)
doc := p.Parse([]byte(result.Content))
if doc != nil && ast.GetFirstChild(doc) != nil {
renderMarkdownToPDF(pdf, tr, doc)
} else {
pdf.MultiCell(0, 10, tr(result.Content), "", "", false)
}
// Save PDF
savedPath := filepath.Join(a.outputDir, filename)
if err := pdf.OutputFileAndClose(savedPath); err != nil {
return types.ActionResult{}, fmt.Errorf("failed to save PDF: %w", err)
}
return types.ActionResult{
Result: fmt.Sprintf("PDF generated and saved to: %s", savedPath),
Metadata: map[string]interface{}{
MetadataPDFs: []string{savedPath},
},
}, nil
}
func (a *GenPDFAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: "generate_pdf",
Description: "Generate a PDF document from text content. The PDF is saved locally and can be sent to the user by connectors.",
Properties: map[string]jsonschema.Definition{
"title": {
Type: jsonschema.String,
Description: "Title of the PDF document",
},
"content": {
Type: jsonschema.String,
Description: "Text or Markdown content to include in the PDF (headings, bold, lists, code blocks, etc. are rendered)",
},
"filename": {
Type: jsonschema.String,
Description: "Optional custom filename (extension is optional - .pdf will be automatically added if missing)",
},
},
Required: []string{"content"},
}
}
func (a *GenPDFAction) Plannable() bool {
return true
}
// GenPDFConfigMeta returns the metadata for GenPDF action configuration fields.
func GenPDFConfigMeta() []config.Field {
return []config.Field{
{
Name: "outputDir",
Label: "Output directory",
Type: config.FieldTypeText,
Required: true,
HelpText: "Directory where generated PDF files are saved",
},
{
Name: "cleanOnStart",
Label: "Clean output directory on start",
Type: config.FieldTypeCheckbox,
DefaultValue: false,
HelpText: "If enabled, clear the output directory when the action is loaded",
},
}
}
+412
View File
@@ -0,0 +1,412 @@
package actions
import (
"fmt"
"strings"
"github.com/gomarkdown/markdown/ast"
"github.com/jung-kurt/gofpdf"
)
const (
pdfLineHeight = 6.0
pdfBlockMargin = 4.0
)
// renderMarkdownToPDF walks the markdown AST and renders it to the PDF using tr for all text.
func renderMarkdownToPDF(pdf *gofpdf.Fpdf, tr func(string) string, doc ast.Node) {
for child := ast.GetFirstChild(doc); child != nil; child = ast.GetNextNode(child) {
renderBlock(pdf, tr, child)
}
}
func renderBlock(pdf *gofpdf.Fpdf, tr func(string) string, node ast.Node) {
switch n := node.(type) {
case *ast.Document:
for child := ast.GetFirstChild(n); child != nil; child = ast.GetNextNode(child) {
renderBlock(pdf, tr, child)
}
case *ast.Heading:
level := n.Level
if level > 6 {
level = 6
}
size := float64(22 - level*2)
if size < 12 {
size = 12
}
pdf.SetFont("Arial", "B", size)
writeInlineContent(pdf, tr, n)
pdf.Ln(pdfLineHeight + pdfBlockMargin)
pdf.SetFont("Arial", "", 12)
case *ast.Paragraph:
writeInlineContent(pdf, tr, n)
pdf.Ln(pdfLineHeight + pdfBlockMargin)
case *ast.List:
listType := n.ListFlags
ordered := (listType & ast.ListTypeOrdered) != 0
start := n.Start
if start <= 0 {
start = 1
}
itemNum := 0
for child := ast.GetFirstChild(n); child != nil; child = ast.GetNextNode(child) {
if item, ok := child.(*ast.ListItem); ok {
itemNum++
var bullet string
if ordered {
bullet = tr(fmt.Sprintf("%d. ", start+itemNum-1))
} else {
bullet = tr("• ")
}
pdf.SetFont("Arial", "", 12)
pdf.CellFormat(8, pdfLineHeight, bullet, "", 0, "", false, 0, "")
for inner := ast.GetFirstChild(item); inner != nil; inner = ast.GetNextNode(inner) {
renderBlock(pdf, tr, inner)
}
}
}
pdf.Ln(pdfBlockMargin)
case *ast.ListItem:
for child := ast.GetFirstChild(n); child != nil; child = ast.GetNextNode(child) {
renderBlock(pdf, tr, child)
}
case *ast.CodeBlock:
pdf.SetFont("Courier", "", 10)
lit := n.Literal
if lit == nil {
lit = n.Content
}
if len(lit) > 0 {
pdf.MultiCell(0, pdfLineHeight-1, tr(string(lit)), "", "", false)
}
pdf.SetFont("Arial", "", 12)
pdf.Ln(pdfBlockMargin)
case *ast.BlockQuote:
left, _, _, _ := pdf.GetMargins()
saveLeft := left
pdf.SetLeftMargin(saveLeft + 4)
pdf.SetX(saveLeft + 4)
for child := ast.GetFirstChild(n); child != nil; child = ast.GetNextNode(child) {
renderBlock(pdf, tr, child)
}
pdf.SetLeftMargin(saveLeft)
pdf.Ln(pdfBlockMargin)
case *ast.HorizontalRule:
pdf.Ln(pdfBlockMargin)
pdf.Line(pdf.GetX(), pdf.GetY(), pdf.GetX()+190, pdf.GetY())
pdf.Ln(pdfBlockMargin)
case *ast.Table:
renderTable(pdf, tr, n)
pdf.Ln(pdfBlockMargin)
case *ast.MathBlock:
pdf.SetFont("Courier", "", 10)
lit := n.Literal
if lit == nil {
lit = n.Content
}
if len(lit) > 0 {
pdf.MultiCell(0, pdfLineHeight-1, tr(string(lit)), "", "", false)
}
pdf.SetFont("Arial", "", 12)
pdf.Ln(pdfBlockMargin)
case *ast.HTMLBlock:
lit := n.Literal
if lit == nil {
lit = n.Content
}
if len(lit) > 0 {
pdf.SetFont("Courier", "", 9)
pdf.MultiCell(0, pdfLineHeight-1, tr(string(lit)), "", "", false)
pdf.SetFont("Arial", "", 12)
}
pdf.Ln(pdfBlockMargin)
case *ast.Aside:
left, _, _, _ := pdf.GetMargins()
saveLeft := left
pdf.SetLeftMargin(saveLeft + 4)
pdf.SetX(saveLeft + 4)
for child := ast.GetFirstChild(n); child != nil; child = ast.GetNextNode(child) {
renderBlock(pdf, tr, child)
}
pdf.SetLeftMargin(saveLeft)
pdf.Ln(pdfBlockMargin)
default:
// Unknown block: try to render as inline content (e.g. paragraph-like)
if ast.GetFirstChild(node) != nil {
writeInlineContent(pdf, tr, node)
pdf.Ln(pdfLineHeight + pdfBlockMargin)
}
}
}
const (
pdfTableLineHt = 7.0
pdfTableHeaderR = 72
pdfTableHeaderG = 72
pdfTableHeaderB = 72
pdfTableBorderR = 200
pdfTableBorderG = 200
pdfTableBorderB = 200
pdfTableStripR = 248
pdfTableStripG = 248
pdfTableStripB = 248
)
// renderTable draws a markdown table. Table contains TableHeader and TableBody, each with TableRows of TableCells.
func renderTable(pdf *gofpdf.Fpdf, tr func(string) string, table *ast.Table) {
left, _, right, _ := pdf.GetMargins()
pageW := 210.0
tblW := pageW - left - right
// Collect all rows: header rows first, then body (and footer if any)
var rows [][]string
var numCols int
for section := ast.GetFirstChild(table); section != nil; section = ast.GetNextNode(section) {
for rowNode := ast.GetFirstChild(section); rowNode != nil; rowNode = ast.GetNextNode(rowNode) {
row, ok := rowNode.(*ast.TableRow)
if !ok {
continue
}
var cells []string
for c := ast.GetFirstChild(row); c != nil; c = ast.GetNextNode(c) {
if cell, ok := c.(*ast.TableCell); ok {
cells = append(cells, tr(getCellText(cell)))
}
}
if len(cells) > 0 {
rows = append(rows, cells)
if len(cells) > numCols {
numCols = len(cells)
}
}
}
}
if numCols == 0 {
return
}
colW := tblW / float64(numCols)
lineHt := pdfTableLineHt
// Save current colors and set light gray borders for the table
saveDrawR, saveDrawG, saveDrawB := pdf.GetDrawColor()
saveFillR, saveFillG, saveFillB := pdf.GetFillColor()
saveTextR, saveTextG, saveTextB := pdf.GetTextColor()
pdf.SetDrawColor(pdfTableBorderR, pdfTableBorderG, pdfTableBorderB)
for i, row := range rows {
isHeader := i == 0
lastRow := i == len(rows) - 1
// Header: dark gray background, white text, bold
if isHeader {
pdf.SetFont("Arial", "B", 12)
pdf.SetFillColor(pdfTableHeaderR, pdfTableHeaderG, pdfTableHeaderB)
pdf.SetTextColor(255, 255, 255)
} else {
pdf.SetFont("Arial", "", 12)
pdf.SetTextColor(0, 0, 0)
if i%2 == 1 {
pdf.SetFillColor(pdfTableStripR, pdfTableStripG, pdfTableStripB)
} else {
pdf.SetFillColor(255, 255, 255)
}
}
border := "LTR"
if lastRow {
border = "LTRB"
}
fill := true
for j, cellText := range row {
w := colW
if j == numCols-1 {
w = 0
}
pdf.CellFormat(w, lineHt, cellText, border, 0, "L", fill, 0, "")
}
pdf.Ln(lineHt)
}
// Restore colors and font
pdf.SetDrawColor(saveDrawR, saveDrawG, saveDrawB)
pdf.SetFillColor(saveFillR, saveFillG, saveFillB)
pdf.SetTextColor(saveTextR, saveTextG, saveTextB)
pdf.SetFont("Arial", "", 12)
}
// getInlineText returns plain text from an inline container (e.g. Image alt text).
func getInlineText(node ast.Node) string {
var b []byte
for child := ast.GetFirstChild(node); child != nil; child = ast.GetNextNode(child) {
if leaf, ok := child.(*ast.Leaf); ok && len(leaf.Literal) > 0 {
b = append(b, leaf.Literal...)
} else if text, ok := child.(*ast.Text); ok {
lit := text.Literal
if lit == nil {
lit = text.Content
}
if len(lit) > 0 {
b = append(b, lit...)
}
} else {
b = append(b, getInlineText(child)...)
}
}
return string(b)
}
// getCellText returns plain text from a table cell (walks Paragraph/Text and Leaf nodes).
func getCellText(node ast.Node) string {
var b []byte
for child := ast.GetFirstChild(node); child != nil; child = ast.GetNextNode(child) {
if leaf, ok := child.(*ast.Leaf); ok && len(leaf.Literal) > 0 {
b = append(b, leaf.Literal...)
} else if text, ok := child.(*ast.Text); ok {
lit := text.Literal
if lit == nil {
lit = text.Content
}
if len(lit) > 0 {
b = append(b, lit...)
}
} else {
b = append(b, getCellText(child)...)
}
}
return string(b)
}
// writeInlineContent outputs inline content (text, strong, emph, code) with correct font changes.
func writeInlineContent(pdf *gofpdf.Fpdf, tr func(string) string, node ast.Node) {
lineHt := pdfLineHeight
left, _, right, _ := pdf.GetMargins()
pageW := 210.0 // A4 mm
maxW := pageW - left - right
for child := ast.GetFirstChild(node); child != nil; child = ast.GetNextNode(child) {
writeInline(pdf, tr, child, lineHt, maxW)
}
}
func writeInline(pdf *gofpdf.Fpdf, tr func(string) string, node ast.Node, lineHt, maxW float64) {
switch n := node.(type) {
case *ast.Text:
lit := n.Literal
if lit == nil {
lit = n.Content
}
if len(lit) > 0 {
cellWrap(pdf, tr(string(lit)), lineHt, maxW)
}
case *ast.Strong:
pdf.SetFont("Arial", "B", 12)
for c := ast.GetFirstChild(n); c != nil; c = ast.GetNextNode(c) {
writeInline(pdf, tr, c, lineHt, maxW)
}
pdf.SetFont("Arial", "", 12)
case *ast.Emph:
pdf.SetFont("Arial", "I", 12)
for c := ast.GetFirstChild(n); c != nil; c = ast.GetNextNode(c) {
writeInline(pdf, tr, c, lineHt, maxW)
}
pdf.SetFont("Arial", "", 12)
case *ast.Code:
lit := n.Literal
if lit == nil {
lit = n.Content
}
if len(lit) > 0 {
pdf.SetFont("Courier", "", 11)
cellWrap(pdf, tr(string(lit)), lineHt, maxW)
pdf.SetFont("Arial", "", 12)
}
case *ast.Link:
for c := ast.GetFirstChild(n); c != nil; c = ast.GetNextNode(c) {
writeInline(pdf, tr, c, lineHt, maxW)
}
if len(n.Destination) > 0 {
pdf.SetFont("Arial", "I", 10)
cellWrap(pdf, tr(" ("+string(n.Destination)+")"), lineHt, maxW)
pdf.SetFont("Arial", "", 12)
}
case *ast.Image:
alt := getInlineText(n)
if alt != "" {
cellWrap(pdf, tr(alt), lineHt, maxW)
}
if len(n.Destination) > 0 {
pdf.SetFont("Arial", "I", 10)
cellWrap(pdf, tr(" [Image: "+string(n.Destination)+"]"), lineHt, maxW)
pdf.SetFont("Arial", "", 12)
}
case *ast.Del:
for c := ast.GetFirstChild(n); c != nil; c = ast.GetNextNode(c) {
writeInline(pdf, tr, c, lineHt, maxW)
}
case *ast.Subscript:
pdf.SetFont("Arial", "", 9)
for c := ast.GetFirstChild(n); c != nil; c = ast.GetNextNode(c) {
writeInline(pdf, tr, c, lineHt, maxW)
}
pdf.SetFont("Arial", "", 12)
case *ast.Superscript:
pdf.SetFont("Arial", "", 9)
for c := ast.GetFirstChild(n); c != nil; c = ast.GetNextNode(c) {
writeInline(pdf, tr, c, lineHt, maxW)
}
pdf.SetFont("Arial", "", 12)
case *ast.Math:
lit := n.Literal
if lit == nil {
lit = n.Content
}
if len(lit) > 0 {
pdf.SetFont("Courier", "", 10)
cellWrap(pdf, tr(string(lit)), lineHt, maxW)
pdf.SetFont("Arial", "", 12)
}
case *ast.Hardbreak:
pdf.Ln(lineHt)
case *ast.Softbreak:
pdf.Ln(lineHt)
default:
if leaf, ok := node.(*ast.Leaf); ok && len(leaf.Literal) > 0 {
cellWrap(pdf, tr(string(leaf.Literal)), lineHt, maxW)
}
}
}
// cellWrap outputs text with word-wrap: splits on spaces and starts a new line when the next word would overflow.
func cellWrap(pdf *gofpdf.Fpdf, s string, lineHt, maxW float64) {
left, _, _, _ := pdf.GetMargins()
words := strings.Fields(s)
for i, word := range words {
wordW := pdf.GetStringWidth(word)
spaceW := 0.0
if i > 0 {
spaceW = pdf.GetStringWidth(" ")
}
x := pdf.GetX()
// If this word (and preceding space) would overflow, start a new line first.
if i > 0 {
if x+spaceW+wordW > maxW && x > left {
pdf.Ln(lineHt)
x = pdf.GetX()
} else {
pdf.CellFormat(spaceW, lineHt, " ", "", 0, "", false, 0, "")
x = pdf.GetX()
}
} else if wordW > 0 && x+wordW > maxW && x > left {
pdf.Ln(lineHt)
x = pdf.GetX()
}
// Single word longer than line width: use MultiCell so it wraps.
if wordW > maxW-left {
pdf.MultiCell(0, lineHt, word, "", "", false)
} else {
if x+wordW > maxW && x > left {
pdf.Ln(lineHt)
}
pdf.CellFormat(wordW, lineHt, word, "", 0, "", false, 0, "")
}
}
}
+212
View File
@@ -0,0 +1,212 @@
package actions_test
import (
"context"
"os"
"path/filepath"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/services/actions"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("GenPDFAction", func() {
var (
tmpDir string
action *actions.GenPDFAction
ctx context.Context
sharedState *types.AgentSharedState
)
BeforeEach(func() {
var err error
tmpDir, err = os.MkdirTemp("", "genpdf_test_*")
Expect(err).ToNot(HaveOccurred())
action = actions.NewGenPDF(map[string]string{
"outputDir": tmpDir,
})
ctx = context.Background()
sharedState = &types.AgentSharedState{}
})
AfterEach(func() {
os.RemoveAll(tmpDir)
})
It("generates PDF with title and content", func() {
result, err := action.Run(ctx, sharedState, types.ActionParams{
"title": "Test Document",
"content": "This is test content for the PDF.",
})
Expect(err).ToNot(HaveOccurred())
Expect(result.Result).To(ContainSubstring("PDF generated and saved to:"))
Expect(result.Metadata).To(HaveKey(actions.MetadataPDFs))
paths := result.Metadata[actions.MetadataPDFs].([]string)
Expect(paths).To(HaveLen(1))
Expect(paths[0]).To(BeAnExistingFile())
})
It("requires content parameter", func() {
_, err := action.Run(ctx, sharedState, types.ActionParams{
"title": "Test",
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("content is required"))
})
It("uses custom filename when provided", func() {
result, err := action.Run(ctx, sharedState, types.ActionParams{
"content": "Test content",
"filename": "custom_name",
})
Expect(err).ToNot(HaveOccurred())
paths := result.Metadata[actions.MetadataPDFs].([]string)
Expect(filepath.Base(paths[0])).To(Equal("custom_name.pdf"))
})
It("generates PDF with content only (no title)", func() {
result, err := action.Run(ctx, sharedState, types.ActionParams{
"content": "Just some content without a title.",
})
Expect(err).ToNot(HaveOccurred())
Expect(result.Result).To(ContainSubstring("PDF generated and saved to:"))
paths := result.Metadata[actions.MetadataPDFs].([]string)
Expect(paths).To(HaveLen(1))
Expect(paths[0]).To(BeAnExistingFile())
})
It("automatically adds .pdf extension if missing", func() {
result, err := action.Run(ctx, sharedState, types.ActionParams{
"content": "Test content",
"filename": "my_document",
})
Expect(err).ToNot(HaveOccurred())
paths := result.Metadata[actions.MetadataPDFs].([]string)
Expect(filepath.Base(paths[0])).To(Equal("my_document.pdf"))
})
It("does not double-add .pdf extension", func() {
result, err := action.Run(ctx, sharedState, types.ActionParams{
"content": "Test content",
"filename": "document.pdf",
})
Expect(err).ToNot(HaveOccurred())
paths := result.Metadata[actions.MetadataPDFs].([]string)
Expect(filepath.Base(paths[0])).To(Equal("document.pdf"))
})
It("requires outputDir to be configured", func() {
actionNoDir := actions.NewGenPDF(map[string]string{})
_, err := actionNoDir.Run(ctx, sharedState, types.ActionParams{
"content": "Test content",
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("outputDir is required"))
})
It("cleans output directory on start if cleanOnStart is enabled", func() {
// Create a test file in the directory
testFile := filepath.Join(tmpDir, "test.txt")
err := os.WriteFile(testFile, []byte("test"), 0644)
Expect(err).ToNot(HaveOccurred())
Expect(testFile).To(BeAnExistingFile())
// Create a new action with cleanOnStart enabled
_ = actions.NewGenPDF(map[string]string{
"outputDir": tmpDir,
"cleanOnStart": "true",
})
// The test file should be deleted
Expect(testFile).ToNot(BeAnExistingFile())
})
It("does not clean output directory if cleanOnStart is disabled", func() {
// Create a test file in the directory
testFile := filepath.Join(tmpDir, "test.txt")
err := os.WriteFile(testFile, []byte("test"), 0644)
Expect(err).ToNot(HaveOccurred())
Expect(testFile).To(BeAnExistingFile())
// Create a new action with cleanOnStart disabled (default)
_ = actions.NewGenPDF(map[string]string{
"outputDir": tmpDir,
})
// The test file should still exist
Expect(testFile).To(BeAnExistingFile())
})
It("prevents path traversal in filename", func() {
result, err := action.Run(ctx, sharedState, types.ActionParams{
"content": "Test content",
"filename": "../../../etc/passwd",
})
Expect(err).ToNot(HaveOccurred())
paths := result.Metadata[actions.MetadataPDFs].([]string)
// Should only use the base filename, not the path
Expect(filepath.Base(paths[0])).To(Equal("passwd.pdf"))
// Should be in the tmpDir, not in /etc
Expect(filepath.Dir(paths[0])).To(Equal(tmpDir))
})
It("generates PDF with markdown content and renders structure", func() {
content := "# Section\n\n**Bold** and *italic* and `code`.\n\n- Item one\n- Item two"
result, err := action.Run(ctx, sharedState, types.ActionParams{
"content": content,
})
Expect(err).ToNot(HaveOccurred())
Expect(result.Result).To(ContainSubstring("PDF generated and saved to:"))
paths := result.Metadata[actions.MetadataPDFs].([]string)
Expect(paths).To(HaveLen(1))
Expect(paths[0]).To(BeAnExistingFile())
info, err := os.Stat(paths[0])
Expect(err).ToNot(HaveOccurred())
Expect(info.Size()).To(BeNumerically(">", 0))
})
It("generates PDF with special characters", func() {
content := "Café, \"quotes\", 23"
result, err := action.Run(ctx, sharedState, types.ActionParams{
"content": content,
})
Expect(err).ToNot(HaveOccurred())
Expect(result.Result).To(ContainSubstring("PDF generated and saved to:"))
paths := result.Metadata[actions.MetadataPDFs].([]string)
Expect(paths).To(HaveLen(1))
Expect(paths[0]).To(BeAnExistingFile())
info, err := os.Stat(paths[0])
Expect(err).ToNot(HaveOccurred())
Expect(info.Size()).To(BeNumerically(">", 0))
})
It("generates PDF with markdown table", func() {
content := "| A | B |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |"
result, err := action.Run(ctx, sharedState, types.ActionParams{
"content": content,
})
Expect(err).ToNot(HaveOccurred())
Expect(result.Result).To(ContainSubstring("PDF generated and saved to:"))
paths := result.Metadata[actions.MetadataPDFs].([]string)
Expect(paths).To(HaveLen(1))
Expect(paths[0]).To(BeAnExistingFile())
info, err := os.Stat(paths[0])
Expect(err).ToNot(HaveOccurred())
Expect(info.Size()).To(BeNumerically(">", 0))
})
})
+302
View File
@@ -0,0 +1,302 @@
package actions
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/dhowden/tag"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/sashabaranov/go-openai/jsonschema"
)
const (
MetadataSongs = "songs_paths"
)
// audioExtensionFromContentType returns a file extension for common audio MIME types.
// It strips parameters (e.g. "audio/flac; rate=44100" -> "flac").
func audioExtensionFromContentType(contentType string) string {
mediaType, _, _ := strings.Cut(strings.TrimSpace(contentType), ";")
mediaType = strings.TrimSpace(strings.ToLower(mediaType))
switch mediaType {
case "audio/flac":
return "flac"
case "audio/mpeg", "audio/mp3":
return "mp3"
case "audio/wav", "audio/wave", "audio/x-wav":
return "wav"
case "audio/ogg":
return "ogg"
case "audio/webm":
return "webm"
default:
return ""
}
}
// audioExtensionFromTag uses github.com/dhowden/tag to identify format from audio bytes.
// Identify works on raw audio (e.g. FLAC without vorbis comments) and returns FileType.
func audioExtensionFromTag(data []byte) string {
if len(data) < 11 {
return ""
}
r := bytes.NewReader(data)
_, fileType, err := tag.Identify(r)
if err != nil || fileType == tag.UnknownFileType {
return ""
}
switch fileType {
case tag.FLAC:
return "flac"
case tag.MP3:
return "mp3"
case tag.OGG:
return "ogg"
case tag.M4A:
return "m4a"
case tag.M4B:
return "m4b"
case tag.M4P:
return "m4p"
case tag.ALAC:
return "m4a"
case tag.DSF:
return "dsf"
default:
return ""
}
}
// soundRequest matches LocalAI /sound endpoint (ACE-Step advanced mode) request body.
// See: https://localai.io/features/text-to-audio/
type soundRequest struct {
Model string `json:"model"`
Caption string `json:"caption"`
Lyrics string `json:"lyrics,omitempty"`
BPM *int `json:"bpm,omitempty"`
Keyscale string `json:"keyscale,omitempty"`
Language string `json:"language,omitempty"`
DurationSeconds *float64 `json:"duration_seconds,omitempty"`
}
func NewGenSong(config map[string]string) *GenSongAction {
model := config["model"]
if model == "" {
model = "ace-step-turbo"
}
a := &GenSongAction{
apiURL: strings.TrimSuffix(config["apiURL"], "/"),
apiKey: config["apiKey"],
outputDir: config["outputDir"],
model: model,
cleanOnStart: config["cleanOnStart"] == "true" || config["cleanOnStart"] == "1",
}
if a.outputDir != "" {
if err := os.MkdirAll(a.outputDir, 0755); err != nil {
// log but continue; Run will fail with a clear error when saving
_ = err
}
if a.cleanOnStart {
entries, err := os.ReadDir(a.outputDir)
if err == nil {
for _, e := range entries {
_ = os.Remove(filepath.Join(a.outputDir, e.Name()))
}
}
}
}
return a
}
type GenSongAction struct {
apiURL string
apiKey string
outputDir string
model string
cleanOnStart bool
}
func (a *GenSongAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Caption string `json:"caption"`
Lyrics string `json:"lyrics"`
BPM *int `json:"bpm"`
Keyscale string `json:"keyscale"`
Language string `json:"language"`
Duration *float64 `json:"duration_seconds"`
Model string `json:"model"`
}{}
if err := params.Unmarshal(&result); err != nil {
return types.ActionResult{}, err
}
if result.Caption == "" {
return types.ActionResult{}, fmt.Errorf("caption is required")
}
if a.outputDir == "" {
return types.ActionResult{}, fmt.Errorf("outputDir is required for generate_song (configure the action with an output directory)")
}
reqBody := soundRequest{
Model: a.model,
Caption: result.Caption,
Lyrics: result.Lyrics,
Keyscale: result.Keyscale,
Language: result.Language,
DurationSeconds: result.Duration,
BPM: result.BPM,
}
body, err := json.Marshal(reqBody)
if err != nil {
return types.ActionResult{}, err
}
url := a.apiURL + "/v1/sound-generation"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return types.ActionResult{}, err
}
req.Header.Set("Content-Type", "application/json")
if a.apiKey != "" {
req.Header.Set("xi-api-key", a.apiKey)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return types.ActionResult{Result: "Failed to generate song: " + err.Error()}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(resp.Body)
return types.ActionResult{}, fmt.Errorf("sound endpoint failed: %s: %s", resp.Status, string(msg))
}
audioBytes, err := io.ReadAll(resp.Body)
if err != nil {
return types.ActionResult{}, err
}
if len(audioBytes) == 0 {
return types.ActionResult{}, fmt.Errorf("no audio data returned")
}
ext := audioExtensionFromContentType(resp.Header.Get("Content-Type"))
if ext == "" {
ext = audioExtensionFromTag(audioBytes)
}
if ext == "" {
ext = "flac" // default when unknown (e.g. ACE-Step)
}
filename := fmt.Sprintf("song_%d.%s", time.Now().UnixNano(), ext)
savedPath := filepath.Join(a.outputDir, filename)
if err := os.WriteFile(savedPath, audioBytes, 0644); err != nil {
return types.ActionResult{}, fmt.Errorf("failed to save song: %w", err)
}
return types.ActionResult{
Result: fmt.Sprintf("The song was generated and saved to: %s", savedPath),
Metadata: map[string]interface{}{
MetadataSongs: []string{savedPath},
},
}, nil
}
func (a *GenSongAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: "generate_song",
Description: "Generate a song or music track using LocalAI /sound endpoint (ACE-Step advanced mode). Uses caption, optional lyrics, BPM, key scale, language and duration. The file is saved locally and can be sent to the user by connectors.",
Properties: map[string]jsonschema.Definition{
"caption": {
Type: jsonschema.String,
Description: "Description of the song or music to generate (e.g. 'A funky Japanese disco track').",
},
"lyrics": {
Type: jsonschema.String,
Description: "Lyrics or structure (e.g. '[Verse 1]\\n...').",
},
"bpm": {
Type: jsonschema.Integer,
Description: "Beats per minute (e.g. 120). Optional.",
},
"keyscale": {
Type: jsonschema.String,
Description: "Key and scale (e.g. 'Ab major'). Optional.",
},
"language": {
Type: jsonschema.String,
Description: "Language code for vocals (e.g. 'ja', 'en'). Optional.",
},
"duration_seconds": {
Type: jsonschema.Number,
Description: "Duration of the generated audio in seconds (e.g. 225). Optional.",
},
"model": {
Type: jsonschema.String,
Description: "Model name (e.g. ace-step-turbo). Optional; uses action config default if omitted.",
},
},
Required: []string{"caption"},
}
}
func (a *GenSongAction) Plannable() bool {
return true
}
// GenSongConfigMeta returns the metadata for GenSong action configuration fields.
func GenSongConfigMeta() []config.Field {
return []config.Field{
{
Name: "apiURL",
Label: "API URL",
Type: config.FieldTypeText,
Required: true,
DefaultValue: "http://localhost:8080",
HelpText: "LocalAI base URL (e.g. http://localhost:8080) for /sound endpoint",
},
{
Name: "model",
Label: "Model",
Type: config.FieldTypeText,
Required: false,
DefaultValue: "ace-step-turbo",
HelpText: "Default model for sound generation (e.g. ace-step-turbo)",
},
{
Name: "apiKey",
Label: "API Key",
Type: config.FieldTypeText,
Required: false,
HelpText: "Optional API key if the endpoint requires authentication",
},
{
Name: "outputDir",
Label: "Output directory",
Type: config.FieldTypeText,
Required: true,
HelpText: "Directory where generated song files are saved (required for connectors to send files)",
},
{
Name: "cleanOnStart",
Label: "Clean output directory on start",
Type: config.FieldTypeCheckbox,
DefaultValue: false,
HelpText: "If enabled, clear the output directory when the action is loaded",
},
}
}
+1 -1
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 {
Repository string `json:"repository"`
Owner string `json:"owner"`
+1 -1
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 {
Repository string `json:"repository"`
Owner string `json:"owner"`
+151
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",
},
}
}
+2 -2
View File
@@ -8,7 +8,7 @@ import (
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/xlog"
"github.com/sashabaranov/go-openai/jsonschema"
)
@@ -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 {
Repository string `json:"repository"`
Owner string `json:"owner"`
+1 -1
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 {
Title string `json:"title"`
Body string `json:"text"`
+1 -1
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 {
Repository string `json:"repository"`
Owner string `json:"owner"`
+2 -2
View File
@@ -7,7 +7,7 @@ import (
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/xlog"
"github.com/sashabaranov/go-openai/jsonschema"
)
@@ -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 {
Query string `json:"query"`
Repository string `json:"repository"`
+1 -93
View File
@@ -3,8 +3,6 @@ package actions
import (
"context"
"fmt"
"regexp"
"strconv"
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
@@ -17,96 +15,6 @@ type GithubPRCommenter struct {
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 {
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 {
Repository string `json:"repository"`
Owner string `json:"owner"`
+102 -17
View File
@@ -3,6 +3,7 @@ package actions
import (
"context"
"fmt"
"time"
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
@@ -10,46 +11,103 @@ import (
"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 {
token, repository, owner, customActionName, defaultBranch string
useFork bool
client *github.Client
}
func NewGithubPRCreator(config map[string]string) *GithubPRCreator {
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{
client: client,
token: config["token"],
repository: config["repository"],
owner: config["owner"],
customActionName: config["customActionName"],
defaultBranch: config["defaultBranch"],
defaultBranch: defaultBranch,
useFork: useFork,
}
}
func (g *GithubPRCreator) createOrUpdateBranch(ctx context.Context, branchName string) error {
// Get the latest commit SHA from the default branch
ref, _, err := g.client.Git.GetRef(ctx, g.owner, g.repository, "refs/heads/"+g.defaultBranch)
// ensureFork ensures that a fork exists for the given repository
func (g *GithubPRCreator) ensureFork(ctx context.Context, owner, repo string) (string, error) {
// First check if we already have a fork
user, _, err := g.client.Users.Get(ctx, "")
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
_, 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 branch doesn't exist, create it
if resp != nil && resp.StatusCode == 404 {
if resp == nil {
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{
Ref: github.String("refs/heads/" + branchName),
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 {
return fmt.Errorf("failed to create branch: %w", err)
}
return nil
}
// For other errors, return the error
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),
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 {
return fmt.Errorf("failed to update branch: %w", err)
}
@@ -66,10 +124,10 @@ func (g *GithubPRCreator) createOrUpdateBranch(ctx context.Context, branchName s
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
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,
})
if err == nil && fileContent != nil {
@@ -77,7 +135,7 @@ func (g *GithubPRCreator) createOrUpdateFile(ctx context.Context, branch string,
}
// 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,
Content: []byte(content),
Branch: &branch,
@@ -90,7 +148,7 @@ func (g *GithubPRCreator) createOrUpdateFile(ctx context.Context, branch string,
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 {
Repository string `json:"repository"`
Owner string `json:"owner"`
@@ -117,15 +175,29 @@ func (g *GithubPRCreator) Run(ctx context.Context, params types.ActionParams) (t
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
err = g.createOrUpdateBranch(ctx, result.Branch)
err = g.createOrUpdateBranch(ctx, result.Branch, targetOwner, targetRepo)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to create/update branch: %w", err)
}
// Create or update 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 {
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
prs, _, err := g.client.PullRequests.List(ctx, result.Owner, result.Repository, &github.PullRequestListOptions{
State: "open",
Head: fmt.Sprintf("%s:%s", result.Owner, result.Branch),
Head: fmt.Sprintf("%s:%s", targetOwner, result.Branch),
})
if err != nil {
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,
}
// 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)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to create pull request: %w", err)
@@ -311,5 +389,12 @@ func GithubPRCreatorConfigMeta() []config.Field {
Required: false,
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.",
},
}
}
+2 -2
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(result.Result).To(ContainSubstring("pull request #"))
})
@@ -65,7 +65,7 @@ var _ = Describe("GithubPRCreator", func() {
"body": "This is a test pull request",
}
_, err := action.Run(ctx, params)
_, err := action.Run(ctx, nil, params)
Expect(err).To(HaveOccurred())
})
})
+1 -1
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 {
Repository string `json:"repository"`
Owner string `json:"owner"`
+2 -2
View File
@@ -9,7 +9,7 @@ import (
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/xlog"
"github.com/sashabaranov/go-openai/jsonschema"
)
@@ -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 {
Repository string `json:"repository"`
Owner string `json:"owner"`
+3 -3
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(result.Result).To(ContainSubstring("reviewed successfully"))
})
@@ -70,7 +70,7 @@ var _ = Describe("GithubPRReviewer", func() {
"review_action": "COMMENT",
}
result, err := reviewer.Run(ctx, params)
result, err := reviewer.Run(ctx, nil, params)
Expect(err).To(HaveOccurred())
Expect(result.Result).To(ContainSubstring("not found"))
})
@@ -85,7 +85,7 @@ var _ = Describe("GithubPRReviewer", func() {
"review_action": "INVALID_ACTION",
}
_, err := reviewer.Run(ctx, params)
_, err := reviewer.Run(ctx, nil, params)
Expect(err).To(HaveOccurred())
})
})
@@ -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 {
Path string `json:"path"`
Repository string `json:"repository"`
@@ -3,11 +3,13 @@ package actions
import (
"context"
"fmt"
"path/filepath"
"strings"
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/xlog"
"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
// 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 {
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 {
if item.GetType() == "dir" {
// Recursively get content for subdirectories
subContent, err := g.getContentRecursively(ctx, item.GetPath())
subContent, err := g.getContentRecursively(ctx, item.GetPath(), owner, repository)
if err != nil {
return "", err
}
result.WriteString(subContent)
} 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
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 {
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
}
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 {
Repository string `json:"repository"`
Owner string `json:"owner"`
@@ -89,7 +122,7 @@ func (g *GithubRepositoryGetAllContent) Run(ctx context.Context, params types.Ac
result.Path = "."
}
content, err := g.getContentRecursively(ctx, result.Path)
content, err := g.getContentRecursively(ctx, result.Path, result.Owner, result.Repository)
if err != nil {
return types.ActionResult{}, err
}

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