Compare commits

...
Author SHA1 Message Date
MervinPraison b151143eb4 Release v0.3.147 2026-09-23 15:42:18 +00:00
praisonai-triage-agent[bot] 5949866103 Merge pull request #303 from MervinPraison/claude/issue-298-20260923
docs: BeautifulUI upgrade playbook and MIT attribution
2026-09-23 15:39:27 +00:00
praisonai-triage-agent[bot] b56bd87903 Merge pull request #300 from MervinPraison/claude/issue-294-20260923
fix: BeautifulUI adapter boundary + upstream pin policy (#294)
2026-09-23 15:39:07 +00:00
praisonai-triage-agent[bot] 0976d1caa3 Merge pull request #301 from MervinPraison/claude/issue-297-20260923
fix: render approvals view as accessible Approval Card (#297)
2026-09-23 15:38:53 +00:00
praisonai-triage-agent[bot] ef4a16ee68 Merge pull request #299 from MervinPraison/claude/issue-292-20260923
docs: BeautifulUI integration strategy (Epic #292)
2026-09-23 15:38:42 +00:00
MervinPraisonandClaude Opus 4.8 a3568bdb31 fix: normalize approval created_at epoch to ISO in agent-UI adapter
The /api/approvals payloads emit created_at as epoch seconds (time.time()),
but mapApprovalToCardProps used asString(), silently dropping the value for
real payloads so ApprovalPromptModel.createdAt was always undefined. Coerce
numeric epochs to an ISO-8601 string (ISO strings still pass through) and
align the unit test with the real wire shape.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-23 14:23:52 +00:00
MervinPraisonandClaude Opus 4.8 ffcb9da9df docs: fix vanilla chat plugin path in BeautifulUI strategy
Correct the fallback reference to templates/frontend/plugins/views/chat.js
(the file lives under views/, not directly under plugins/).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-23 14:23:22 +00:00
MervinPraisonandClaude Opus 4.8 afce1e542d docs: wire BeautifulUI playbook into aiui.template.yaml navigation
The shipped docs site is built by `aiui build` from aiui.template.yaml
(see ci.yml / deploy-pages.yml), not mkdocs.yml. Add a Frontend nav group
so the playbook is discoverable in the built site navigation.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-23 14:11:45 +00:00
MervinPraisonandClaude Opus 4.8 4d8f9b0322 fix: persist approval custom answer/option via reason field (#297)
The approve/deny endpoints only store `reason`; map the selected option or
typed custom answer into it so the human's decision note is recorded, while
keeping `option`/`custom_answer` for a future richer backend.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-23 14:10:13 +00:00
MervinPraisonandClaude Opus 4.8 a3a5e06225 test: make agent-UI adapter tests runnable via npm test (#294)
The adapter unit tests use Node's built-in runner, but the documented
command (`node --test src/agent-ui/`) fails on Node 22 — it neither
strips TypeScript types nor discovers the files, so the suite never ran.

- Add a `test` script to src/frontend that runs the suite with
  `--experimental-strip-types` (8/8 passing on Node 22).
- Fix the misleading run instructions in the test header.

Closes the "unit tests" acceptance criterion of #294 with an executable
suite. Purely additive; no runtime code changed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-23 14:07:50 +00:00
MervinPraisonandClaude Opus 4.8 1807595cd0 docs: add BeautifulUI upgrade playbook and MIT attribution (fixes #298)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-23 13:52:35 +00:00
MervinPraisonandClaude Opus 4.8 aeefceeaa2 fix: render approvals view as accessible Approval Card (fixes #297)
- Add self-contained toApprovalModel() normalizer (ApprovalPromptModel shape)
- Render pending items as an Approval Card: risk accent, question, options,
  custom answer and confidence copy — surfaced only when the API supplies them
- Preserve /api/approvals approve|deny endpoints and skill-approval deep links
- Escape all interpolated API values via esc() (fixes prior XSS gap)
- Improve a11y: role=group/radiogroup, aria-labels on buttons/inputs

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-23 13:49:18 +00:00
MervinPraisonandClaude Opus 4.8 5b191a439b fix: add BeautifulUI adapter boundary + upstream pin policy (fixes #294)
Introduces the agent-UI adapter layer so BeautifulUI can be vendored
without leaking upstream types/paths across the frontend.

- src/frontend/src/agent-ui/contracts.ts: stable PraisonAIUI view models
  (AgentStreamEvent, ToolCallChipModel, ApprovalPromptModel)
- src/frontend/src/agent-ui/adapter.ts: pure mappers mapChatEventToUi()
  and mapApprovalToCardProps() from WS frames / approvals REST payloads
- src/frontend/src/agent-ui/adapter.test.ts: dependency-free unit tests
  (node:test, mock events -> props); 8 tests pass via `node --test`
- docs/frontend/: ADR (vendoring, adapter boundaries, folder layout,
  default-off feature flag + fallback) and upstream SHA/pin record
- mkdocs.yml: nav entries for the new frontend docs

Boundary rule: BeautifulUI prop types are imported only inside
src/agent-ui/; consumers depend on contracts.ts, keeping the vendored
implementation swappable. No new runtime deps; adapter is not yet
mounted (migration is #296/#297, CI gate is #295).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-23 13:47:39 +00:00
MervinPraisonandClaude Opus 4.8 0746ef5e6c docs: add BeautifulUI integration strategy (fixes #292)
Adds the durable integration strategy for the BeautifulUI epic: vendored
source (no runtime npm dep), adapter layer behind @praisonaiui/react,
phased flag-gated rollout, selective token merge, and lucide-react for
nav icons. Documents risks and links the child issues (#293-#298).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-23 13:41:43 +00:00
MervinPraisonandCursor 3058886312 chore: bump automation hub to v1.1.10 for merge gate fix
Co-authored-by: Cursor <[email protected]>
2026-09-22 15:30:07 +01:00
Mervin Praison 70d8610733 Merge pull request #291 from MervinPraison/claude/issue-290-20260916
docs: add SECURITY.md with private disclosure policy
2026-09-17 10:37:03 +01:00
MervinPraison 0f60ec41f1 Release v0.3.146 2026-09-17 03:02:10 +00:00
MervinPraisonandClaude Opus 4.8 2797667e04 docs: add SECURITY.md with private disclosure policy (fixes #290)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-16 16:28:01 +00:00
Mervin Praison f23aacdec9 Merge pull request #289 from MervinPraison/claude/issue-286-20260915
fix: auto-load ~/.praisonai/.env on aiui run so env: channel tokens resolve
2026-09-16 14:43:31 +01:00
MervinPraison 17cd4d9f86 Release v0.3.145 2026-09-16 13:00:52 +00:00
Mervin Praison 43935e4e41 fix: forward extra kwargs through channels chat bridge (fixes #287)
fix: forward extra kwargs through channels chat bridge (fixes #287)
2026-09-16 13:58:08 +01:00
MervinPraisonandClaude Opus 4.8 8dc8b9f49f fix: auto-load ~/.praisonai/.env on aiui run so env: tokens resolve (fixes #286)
Channels configured with env:VAR token references silently resolved to
empty strings when the dashboard was started without the shell exporting
the tokens, so bots showed 'Connected' but never replied. aiui run now
loads the standard PraisonAI .env files (~/.praisonai/.env, then ./.env)
into the process environment at startup, matching the PraisonAI CLI
convention. Existing process vars are never overridden and parsing is
dependency-free (no python-dotenv).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-15 13:16:37 +00:00
MervinPraisonandClaude Opus 4.8 135b05435c fix: forward extra kwargs through channels chat bridge (fixes #287)
The chat bridge installed over bot._session.chat used a fixed
(agent, user_id, text) signature. When the bot internals invoked the
session with per-conversation keyword arguments (chat_id, platform,
thread_id, ...) the wrapper raised
`TypeError: _wrapped_chat() got an unexpected keyword argument 'chat_id'`
and the bot silently failed to reply across all platforms (Telegram,
Slack, Discord).

Add `*args, **kwargs` to `_wrapped_chat` and forward them untouched to
the original session chat so memory/session continuity is preserved.
Minimal, backward-compatible change; no new params or dependencies.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-15 13:11:51 +00:00
MervinPraisonandCursor be502d823f chore(automation): bump hub to v1.1.8 for dual OAuth failover
Uses pick-oauth-token pre-flight probe and TOKEN_B backup.

Co-authored-by: Cursor <[email protected]>
2026-09-15 07:49:29 +01:00
Mervin Praison 9be1266747 Merge pull request #285 from MervinPraison/fix/voice-doctor-mcp-probe-timeout
fix(voice-agent): warn on slow MCP doctor probe instead of failing
2026-09-14 17:45:00 +01:00
MervinPraisonandCursor 8ce13a9fec fix(voice-agent): warn on slow MCP doctor probe instead of failing
Use a 2s timeout for GET /api/mcp/servers and downgrade timeout-style
errors to warn so stdio MCP connect does not fail the whole doctor run.

Follow-up to #273 (example-only; no core SDK changes).

Co-authored-by: Cursor <[email protected]>
2026-09-14 14:51:08 +01:00
Mervin Praison e6a346aa22 Merge pull request #284 from MervinPraison/chore/automation-hub-v1.1.7
chore(automation): bump github-automation-template hub to v1.1.7
2026-09-14 14:22:51 +01:00
32 changed files with 1252 additions and 58 deletions

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@
"stack": "claude",
"profile": "sdk",
"mode": "hub",
"hubRef": "v1.1.7",
"hubRef": "v1.1.8",
"templateVersion": "1.1.7",
"installedAt": "2026-09-07T10:02:11Z"
}
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
permissions:
issues: write
pull-requests: write
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].7
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
secrets: inherit
with:
hub_ref: v1.1.7
hub_ref: v1.1.10
+2 -2
View File
@@ -16,8 +16,8 @@ permissions:
jobs:
recover:
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].7
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
secrets: inherit
with:
hub_ref: v1.1.7
hub_ref: v1.1.10
pr_number: ${{ github.event.inputs.pr_number || '' }}
+2 -2
View File
@@ -25,8 +25,8 @@ permissions:
jobs:
detect-and-trigger:
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].7
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
secrets: inherit
with:
hub_ref: v1.1.7
hub_ref: v1.1.10
pr_number: ${{ github.event.inputs.pr_number || '' }}
+4 -4
View File
@@ -43,9 +43,9 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].7
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].10
with:
hub_ref: v1.1.7
hub_ref: v1.1.10
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
@@ -87,9 +87,9 @@ jobs:
issues: write
actions: read
id-token: write
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].7
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
secrets: inherit
with:
hub_ref: v1.1.7
hub_ref: v1.1.10
pr_number: ${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || '' }}
merge_method: ${{ github.event.inputs.merge_method || 'merge' }}
+2 -2
View File
@@ -18,7 +18,7 @@ jobs:
issues: write
actions: read
id-token: write
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].7
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
secrets: inherit
with:
hub_ref: v1.1.7
hub_ref: v1.1.10
+2 -2
View File
@@ -25,9 +25,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].7
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].10
with:
hub_ref: v1.1.7
hub_ref: v1.1.10
- name: Detect conflicting PRs and trigger Claude
uses: actions/github-script@v7
with:
+2 -2
View File
@@ -13,7 +13,7 @@ permissions:
jobs:
sync:
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].7
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
secrets: inherit
with:
hub_ref: v1.1.7
hub_ref: v1.1.10
+4
View File
@@ -366,3 +366,7 @@ pytest tests -v
## License
MIT © [Praison Limited](https://praison.ai)
Third-party vendored source is documented in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
See the [BeautifulUI upgrade playbook](docs/frontend/beautifului.md) for how BeautifulUI
primitives are vendored and pinned.
+37
View File
@@ -0,0 +1,37 @@
# Security Policy
We take the security of PraisonAIUI seriously and appreciate responsible
disclosure of vulnerabilities.
## Supported Versions
Security fixes are applied to the latest released version of PraisonAIUI.
Please make sure you are on the most recent release before reporting.
## Reporting a Vulnerability
**Please do not open a public issue for security vulnerabilities.**
Report privately through GitHub's **Private Vulnerability Reporting**:
1. Go to the [Security tab](https://github.com/MervinPraison/PraisonAIUI/security)
of this repository.
2. Click **"Report a vulnerability"** to open a private advisory.
3. Include as much detail as you can:
- Affected component(s) and version.
- Steps to reproduce.
- Impact assessment.
- Any suggested fix or mitigation.
If you are unable to use private reporting, you may open a public issue that
contains **no technical specifics** and requests a private channel, and a
maintainer will follow up.
## What to Expect
- We aim to acknowledge new reports within a few business days.
- We will keep you informed as we investigate and work on a fix.
- Once a fix is released, we are happy to credit you in the advisory (unless
you prefer to remain anonymous).
Thank you for helping keep PraisonAIUI and its users safe.
+43
View File
@@ -0,0 +1,43 @@
# Third-Party Notices
PraisonAIUI incorporates or vendors source from the third-party projects listed below.
Their licences and copyright notices are retained here as required.
---
## BeautifulUI
- Project: BeautifulUI — <https://www.beautifului.dev/>
- Source: <https://github.com/slev12397/beautiful-ui>
- Licence: MIT
BeautifulUI is distributed as a shadcn registry / copy-paste library (no npm package).
When its React primitives are vendored into `src/frontend`, the following MIT licence
applies to that copied source.
```
MIT License
Copyright (c) Shane Levine
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
See the upgrade playbook at [`docs/frontend/beautifului.md`](docs/frontend/beautifului.md)
for how BeautifulUI is vendored and pinned.
+5
View File
@@ -59,6 +59,11 @@ navigation:
- typescript/runtime
- typescript/nextjs
- typescript/components
- group: "Frontend"
icon: "layout"
prefix: "frontend"
pages:
- frontend/beautifului
- group: "Examples"
icon: "folder-open"
prefix: "examples"
+56
View File
@@ -0,0 +1,56 @@
# BeautifulUI Integration Strategy
> **Epic:** [#292](https://github.com/MervinPraison/PraisonAIUI/issues/292) — BeautifulUI integration strategy for AI-native dashboard UX.
> This is the durable, agreed strategy that the linked child issues execute against.
[BeautifulUI](https://www.beautifului.dev/) (MIT, Shane Levine / Turbo) is a **copy-paste / shadcn-registry** library of React primitives for AI-native interfaces: thinking traces, streaming answers with inline sources, HITL approval cards, tool chips, task rows, context chunks, diff/records tables, prompt bars, and a reference chat harness.
There is **no published npm package** named `beautiful-ui`. It ships only as source consumed via the shadcn registry or manual copy.
## 1. Strategic decision
BeautifulUI is adopted as **vendored source**, not a runtime dependency, and is exposed to dashboard plugins **only through the existing `@praisonaiui/react` package** — never by importing upstream file paths directly. Rollout is **phased behind a feature flag**, keeping the current vanilla dashboard as the fallback until parity is proven.
Upstream stack (Next.js 15 · React 19 · Tailwind v4 · TypeScript) is closely aligned with PraisonAIUI's `src/frontend` (Vite · React 19 · Tailwind v4 · shadcn/Radix), so primitives port with adapter work rather than a runtime bridge.
## 2. Pillars
| # | Pillar | Decision |
|---|--------|----------|
| 1 | **Vendoring model** | Add primitives into `src/frontend` via `npx shadcn add https://www.beautifului.dev/r/<component>.json`, or manual copy from [github.com/slev12397/beautiful-ui](https://github.com/slev12397/beautiful-ui). Pin via lockfile plus a recorded registry URL / git SHA. No `beautiful-ui` npm dependency in production builds. |
| 2 | **Adapter layer** | Map PraisonAIUI chat / approval / trace **protocol events** to stable internal props. Primitives sit behind `@praisonaiui/react` exports; dashboard plugins consume those exports, not upstream paths. See [#294](https://github.com/MervinPraison/PraisonAIUI/issues/294). |
| 3 | **Hybrid rollout** | Mount React **islands** (or expand `@praisonaiui/react`) for high-value surfaces first — chat streaming/thinking/tools, then approvals. Vanilla chat (`templates/frontend/plugins/views/chat.js`) stays as the flag-gated fallback until parity is proven. |
| 4 | **Token strategy** | Merge BeautifulUI foundation CSS **selectively** into the existing shadcn theme generation (`praisonaiui/themes.py`). One design system per page — avoid two competing token sets. |
| 5 | **Icon licence** | Upstream `SidebarNav` uses the paid `@central-icons-react`. Port nav patterns with **lucide-react** (already in PraisonAIUI) instead. |
## 3. Phasing
1. **Spike** — evaluate primitives in the dev environment, confirm React 19 / Tailwind v4 alignment. [#293](https://github.com/MervinPraison/PraisonAIUI/issues/293)
2. **Architecture** — adapter layer + upstream pin policy. [#294](https://github.com/MervinPraison/PraisonAIUI/issues/294)
3. **CI gate** — frontend build + BeautifulUI compatibility check (no frontend build gate exists today). [#295](https://github.com/MervinPraison/PraisonAIUI/issues/295)
4. **Migrations** — chat streaming / thinking / tool chips ([#296](https://github.com/MervinPraison/PraisonAIUI/issues/296)), then the approval card ([#297](https://github.com/MervinPraison/PraisonAIUI/issues/297)).
5. **Docs** — upgrade playbook + MIT attribution. [#298](https://github.com/MervinPraison/PraisonAIUI/issues/298)
## 4. Risks
| Risk | Mitigation |
|------|------------|
| No semver npm package | Vendor the source; pin to a recorded registry URL / git SHA in the lockfile. |
| Upstream breaking registry JSON | Vendoring decouples builds from live registry; re-sync is a deliberate, reviewed step. |
| Icon licence (`@central-icons-react` is paid) | Use lucide-react for ported nav patterns; never vendor the paid package. |
| Two competing design systems on one page | Selective CSS token merge into `themes.py`; adapter-only prop surface. |
| React / Tailwind drift | Upstream (React 19 / Tailwind v4) already matches `src/frontend`; the CI gate catches drift. |
## 5. Out of scope
- Replacing the entire dashboard shell in one PR.
- PyPI packaging of BeautifulUI.
- iframe embedding of beautifului.dev.
- Depending on any unpublished npm package for production builds.
## 6. Acceptance criteria status
- [x] Written integration strategy agreed (vendoring + adapter + phased rollout) — this document.
- [x] Risks documented — section 4.
- [x] Child issues filed and prioritised — [#293](https://github.com/MervinPraison/PraisonAIUI/issues/293)–[#298](https://github.com/MervinPraison/PraisonAIUI/issues/298).
- [x] No requirement to depend on unpublished npm packages for production builds — pillar 1, out of scope.
+35
View File
@@ -0,0 +1,35 @@
# BeautifulUI upstream pin record
BeautifulUI is vendored (shadcn-style registry copy), **not** an npm dependency.
This file is the single source of truth for what upstream revision is vendored.
Update it in the **same PR** that adds or bumps any vendored component, and make
sure the frontend CI gate (issue #295) passes.
See [beautifului-adapter-architecture.md](./beautifului-adapter-architecture.md)
for the adapter boundary and folder layout.
## Pin
| Field | Value |
|-------|-------|
| Upstream repo | _TBD — record on first vendor_ |
| Git SHA | _TBD_ |
| Vendored on (date) | _TBD_ |
| Registry base URL | _TBD_ |
| License | MIT (kept in-tree) |
## Vendored components
Record one row per component the first time it is copied in.
| Component | Registry URL | SHA | Vendored path | Date |
|-----------|--------------|-----|---------------|------|
| _(none yet)_ | | | | |
## Upgrade checklist
1. Update the SHA / date / registry rows above.
2. Re-copy changed components into `src/frontend/src/agent-ui/vendors/beautifului/`.
3. Keep all upstream imports inside `src/agent-ui/` (adapter boundary rule).
4. Run the frontend build + adapter unit tests (frontend CI gate, #295).
5. Do **not** add a `beautiful-ui` entry to `package.json` dependencies.
@@ -0,0 +1,102 @@
# ADR: BeautifulUI adapter layer & upstream pin policy
**Status:** Accepted · **Issue:** [#294](https://github.com/MervinPraison/PraisonAIUI/issues/294)
## Context
BeautifulUI is **not** a semver npm library — it is distributed as shadcn-style
registry components that are copied into the consuming repo. PraisonAIUI must
integrate it without leaking upstream types or import paths across the codebase,
and without breaking when either side updates.
## Decision
Introduce a single **adapter boundary** between transport payloads and the UI,
and vendor BeautifulUI components behind that boundary.
```
Agent APIs (Python) ──► event bus / WS messages
│
▼
Agent-UI adapter (src/agent-ui/adapter.ts)
(stable PraisonAIUI contracts)
│
▼
@praisonaiui/react re-exports
(BeautifulUI vendored components)
│
▼
Dashboard / chat mount points (React islands, then full shell)
```
### 1. Folder layout
```
src/frontend/src/agent-ui/
├── contracts.ts # Stable PraisonAIUI view models (the ONLY types consumers import)
├── adapter.ts # Pure mappers: wire payload -> contracts
├── adapter.test.ts # Unit tests (mock events -> props), node:test
├── index.ts # Barrel: contracts + adapters + (later) vendored re-exports
└── vendors/
└── beautifului/ # Vendored BeautifulUI components (added during migration)
```
**Rule:** BeautifulUI / upstream prop types must **never** be imported outside
`src/agent-ui/`. Consumers depend on `contracts.ts` only, so the vendored
implementation can be swapped later without touching call sites.
### 2. Adapter contracts
Internal, transport-agnostic types live in `contracts.ts`:
- `AgentStreamEvent` — normalized form of the WebSocket frames handled in
`chat.js` (`run_started`, `run_content`, `tool_call_started`, …). Unknown
frame `type`s degrade to `kind: 'unknown'` with `rawType` preserved instead
of throwing, so new upstream events never break the UI.
- `ToolCallChipModel` — tool-call chip view model.
- `ApprovalPromptModel` — approval card view model, mapped from the
`/api/approvals/pending` REST payload.
Mappers (`adapter.ts`): `mapChatEventToUi(frame)` and
`mapApprovalToCardProps(item)`. Both are **pure** and unit tested.
### 3. Vendoring & pin policy
| Mechanism | Purpose |
|-----------|---------|
| [`BEAUTIFULUI_UPSTREAM.md`](./BEAUTIFULUI_UPSTREAM.md) | Record git SHA, date, registry base URL for every vendored component |
| shadcn registry URLs | Per-component install, pinned in the upstream doc |
| **No** `dependencies.beautiful-ui` in `package.json` | Avoid a phantom npm package |
**Upgrade rule:** bump upstream **only** via a PR that updates
`BEAUTIFULUI_UPSTREAM.md` and passes the frontend CI gate (issue #295). Icons
stay on `lucide-react`; do not vendor paid Central icons.
### 4. Feature flag (default **off**)
Integration is gated so the existing vanilla-JS chat/approvals plugins remain
the fallback until migration milestones (#296, #297) complete.
- **Flag:** `AIUI_BEAUTIFULUI` (build-time env / dashboard config JSON).
- **Default:** off — vanilla JS renders; the adapter is imported but not mounted.
- **On:** React islands mount using `src/agent-ui` contracts.
- **Fallback:** if a vendored component fails to load, the flag is treated as
off and the vanilla path renders. No user-facing regression when off.
### 5. Design tokens
Scope agent-UI primitives under an `.aiui-agent-ui { … }` CSS layer (or extend
the existing shadcn presets in `themes.py`) so vendored component tokens cannot
collide with the global dashboard theme.
### 6. Deprecation / upstream abandonment
Vendored code stays MIT-licensed in-tree. Because consumers depend only on the
`contracts.ts` view models, a future first-party implementation can satisfy the
same contracts and replace the vendored components with no call-site churn.
## Consequences
- One place (`src/agent-ui/`) owns the upstream coupling.
- Adapters are testable without a browser or the transport layer.
- The default-off flag keeps the migration reversible at every step.
+107
View File
@@ -0,0 +1,107 @@
# BeautifulUI Upgrade Playbook
How PraisonAIUI vendors [BeautifulUI](https://www.beautifului.dev/) React primitives so
upgrades are **repeatable** and licence obligations are **explicit**.
!!! info "Related planning"
Epic [#292](https://github.com/MervinPraison/PraisonAIUI/issues/292) (integration
strategy) and the adapter/pin policy in
[#294](https://github.com/MervinPraison/PraisonAIUI/issues/294).
## 1. What BeautifulUI is
BeautifulUI (MIT, © Shane Levine / Turbo) is a library of React primitives aimed at
**AI-native interfaces** — thinking traces, streaming answers with inline sources,
human-in-the-loop approval cards, tool chips, task rows, context chunks, and a reference
chat harness.
**It is a shadcn registry + copy-paste library — not an npm package.** There is no
published `beautiful-ui` package. You install components by resolving a registry URL, and
the source is copied into your tree (like any other shadcn/ui primitive).
- Website & registry: <https://www.beautifului.dev/>
- Upstream source: <https://github.com/slev12397/beautiful-ui>
Upstream stack (Next.js 15 · React 19 · Tailwind CSS v4 · TypeScript) is closely aligned
with PraisonAIUI's `src/frontend` (Vite · React 19 · Tailwind v4 · shadcn/Radix), which is
why copy-paste vendoring works without a framework shim.
## 2. One-time foundation setup
PraisonAIUI already carries the shadcn/ui foundation that BeautifulUI primitives depend on:
- **shadcn config** — `src/frontend/components.json` (`style: new-york`,
`baseColor: neutral`, `iconLibrary: lucide`, `cssVariables: true`). Its `registries`
map is where pinned BeautifulUI registry URLs are added (see §3).
- **Theme tokens** — `src/praisonaiui/themes.py` generates the shadcn CSS variables
(`--background`, `--foreground`, `--primary`, `--radius`, …) at build time for all 22
official presets, offline. BeautifulUI's foundation CSS (upstream `app/globals.css`)
must only contribute **new** tokens/utilities; it must **not** redefine variables that
`themes.py` already owns, or theme switching and dark mode break.
**Rule:** when merging upstream `app/globals.css`, diff it against the variables emitted by
`themes.py` and keep only the additive slices (new keyframes, component-scoped utilities).
Record what was merged in the pin doc (§3).
## 3. Adding or updating a primitive
1. **Install with a pinned registry URL** from `src/frontend/`:
```bash
npx shadcn add https://www.beautifului.dev/r/<component>.json
```
The registry resolves dependencies and CSS slices. Commit the resulting
`package-lock.json` change in the same PR.
2. **Re-export through the adapter** — add the component to
`src/frontend/src/components/index.ts` (`@praisonaiui/react`). Consumers never import
the raw vendored path (see §4).
3. **Update the pin record** — create/update `docs/frontend/BEAUTIFULUI_UPSTREAM.md` with:
the upstream **git SHA**, the **date**, the **registry base URL**, and the **component
list** vendored so far. This file is the single source of truth for "what version are
we on". (It is created the first time a real primitive is vendored — do not add it
empty.)
4. **Rebuild & sync the bundle** — the committed frontend bundle must be regenerated:
```bash
cd src/frontend && npm run build
# then sync dist/{index.html,assets/,icon.svg} → src/praisonaiui/templates/frontend/
```
(See `AGENTS.md` → "Frontend Build".)
5. **Run frontend CI** — `.github/workflows/ci.yml` (ruff + pytest) must pass, plus the
frontend build step.
## 4. Adapter boundary
Consumers import from **`@praisonaiui/react` only** — never from raw vendored paths such as
`@/components/ui/...` or any upstream module.
```tsx
// ✅ stable PraisonAIUI contract
import { Button, ThemeToggle } from '@praisonaiui/react'
// ❌ leaks upstream/shadcn paths across the codebase
import { Button } from '@/components/ui/button'
```
This keeps the upstream surface behind one file (`components/index.ts`), so a BeautifulUI
bump changes vendored source but not consumer imports.
## 5. Rollback
- BeautifulUI-backed views mount behind a **feature flag**; the **vanilla dashboard views**
(`src/praisonaiui/templates/frontend/plugins/*`) remain the fallback and are kept working.
- To roll back a bad bump: revert the vendoring PR (source + `BEAUTIFULUI_UPSTREAM.md` +
rebuilt bundle move together in one commit), and disable the flag. No consumer import
changes are needed because everything routes through the adapter (§4).
## 6. Licence
BeautifulUI is **MIT, © Shane Levine**. The full licence text is retained in
[`THIRD_PARTY_NOTICES.md`](https://github.com/MervinPraison/PraisonAIUI/blob/main/THIRD_PARTY_NOTICES.md)
at the repository root. Any PR that vendors or bumps BeautifulUI source must keep that
notice present and current.
## 7. Commercial icons — do not vendor
Do **not** use `@central-icons-react` without a licence — it is commercial. PraisonAIUI
standardises on **`lucide-react`** (`iconLibrary: lucide` in `components.json`). If an
upstream BeautifulUI primitive imports commercial icons, swap them for lucide equivalents
during vendoring.
@@ -17,6 +17,25 @@ def _tcp_open(host: str, port: int, timeout: float = 2.0) -> bool:
return False
_MCP_PROBE_TIMEOUT_SEC = 2.0
def _is_slow_mcp_probe(detail: str) -> bool:
lowered = detail.lower()
return any(token in lowered for token in ("timed out", "timeout", "temporarily unavailable"))
def _mcp_servers_probe(app_port: int) -> tuple[str, str]:
"""Probe MCP list endpoint; slow stdio connect must not fail the whole doctor run."""
status, detail = _http_status(
f"http://127.0.0.1:{app_port}/api/mcp/servers",
timeout=_MCP_PROBE_TIMEOUT_SEC,
)
if status == "fail" and _is_slow_mcp_probe(detail):
return "warn", detail
return status, detail
def _http_status(url: str, timeout: float = 5.0) -> tuple[str, str]:
try:
req = Request(url, headers={"User-Agent": "praisonai-voice-doctor"})
@@ -64,7 +83,7 @@ def run_voice_doctor(*, app_port: int = 8001, sidecar_port: int = 8002) -> dict[
checks.append({"name": "Speech Engine config", "status": status, "detail": detail})
status, detail = _http_status(f"http://127.0.0.1:{app_port}/api/voice/realtime/config")
checks.append({"name": "GPT Realtime config", "status": status, "detail": detail})
status, detail = _http_status(f"http://127.0.0.1:{app_port}/api/mcp/servers")
status, detail = _mcp_servers_probe(app_port)
checks.append({"name": "MCP servers API", "status": status, "detail": detail})
else:
checks.append(
+6
View File
@@ -95,6 +95,9 @@ nav:
- Templates & Layouts: concepts/templates.md
- Slots & Components: concepts/slots.md
- Providers: concepts/providers.md
- Frontend:
- BeautifulUI Adapter: frontend/beautifului-adapter-architecture.md
- BeautifulUI Upstream Pin: frontend/BEAUTIFULUI_UPSTREAM.md
- Features:
- Dashboard: features/dashboard.md
- Gateway Chat: features/gateway-chat.md
@@ -129,12 +132,15 @@ nav:
- Feature Inventory: features/feature-inventory.md
- Gap Analysis: features/gap-analysis.md
- Integration Analysis: features/integration-analysis.md
- BeautifulUI Integration: features/beautifului-integration.md
- Standardization: features/standardization-verification.md
- Comparison (OpenClaw): features/comparison-openclaw.md
- What Makes It Different: features/differentiation.md
- How to Add a Feature: features/how-to-add-a-feature.md
- Message Elements: features/message-elements.md
- Steps: features/steps.md
- Frontend:
- BeautifulUI Playbook: frontend/beautifului.md
- API Reference:
- Python CLI: api/cli.md
- Python API: api/python.md
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "aiui"
version = "0.3.144"
version = "0.3.147"
description = "YAML-driven website generator - CLI and compiler"
readme = "README.md"
license = {text = "MIT"}
+2 -1
View File
@@ -29,7 +29,8 @@
"build": "tsc -b && vite build",
"build:lib": "tsc -b && vite build --mode lib",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"test": "node --experimental-strip-types --test \"src/agent-ui/**/*.test.ts\""
},
"keywords": [
"react",
+104
View File
@@ -0,0 +1,104 @@
/**
* Unit tests for the Agent-UI adapter (mock events -> stable contracts).
*
* Dependency-free: uses the Node built-in test runner so it needs no extra
* toolchain. Run with `npm test` (from src/frontend), which invokes the Node
* test runner with `--experimental-strip-types` so the TypeScript types are
* stripped on Node 22+. The frontend CI gate (see issue #295) wires this in.
*/
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { mapApprovalToCardProps, mapChatEventToUi } from './adapter.ts'
test('mapChatEventToUi normalizes a streaming token frame', () => {
const event = mapChatEventToUi({
type: 'run_content',
token: 'Hello',
agent_name: 'Researcher',
session_id: 's1',
run_id: 'r1',
})
assert.equal(event.kind, 'run-token')
assert.equal(event.text, 'Hello')
assert.equal(event.agentName, 'Researcher')
assert.equal(event.sessionId, 's1')
assert.equal(event.runId, 'r1')
})
test('mapChatEventToUi maps a started tool call to a running chip', () => {
const event = mapChatEventToUi({
type: 'tool_call_started',
name: 'search',
description: '🔎 Searching the web',
icon: '🔎',
step_number: 2,
tool_call_id: 'tc-1',
})
assert.equal(event.kind, 'tool-call')
assert.deepEqual(event.toolCall, {
id: 'tc-1',
label: '🔎 Searching the web',
icon: '🔎',
stepNumber: 2,
status: 'running',
error: undefined,
})
})
test('mapChatEventToUi marks completed tool calls as done', () => {
const event = mapChatEventToUi({ type: 'tool_call_completed', name: 'search' })
assert.equal(event.toolCall?.status, 'done')
// Falls back to the tool name for id and label when unspecified.
assert.equal(event.toolCall?.id, 'search')
assert.equal(event.toolCall?.label, 'search')
})
test('mapChatEventToUi treats team_* frames the same as their solo variant', () => {
assert.equal(mapChatEventToUi({ type: 'team_run_content', token: 'x' }).kind, 'run-token')
assert.equal(mapChatEventToUi({ type: 'team_run_error', error: 'boom' }).kind, 'run-error')
})
test('mapChatEventToUi preserves unknown frame types without throwing', () => {
const event = mapChatEventToUi({ type: 'future_event', foo: 'bar' })
assert.equal(event.kind, 'unknown')
assert.equal(event.rawType, 'future_event')
})
test('mapChatEventToUi defaults run errors to a readable message', () => {
const event = mapChatEventToUi({ type: 'run_error' })
assert.equal(event.kind, 'run-error')
assert.equal(event.error, 'Unknown error')
})
test('mapApprovalToCardProps maps a pending approval item', () => {
const card = mapApprovalToCardProps({
id: 'a-1',
tool_name: 'delete_file',
agent_name: 'Ops',
risk_level: 'high',
risk_icon: '🔥',
description: 'Deletes a file',
arguments: { path: '/tmp/x' },
// Approvals API emits epoch seconds (time.time()), not an ISO string.
created_at: 1758585600,
})
assert.equal(card.id, 'a-1')
assert.equal(card.toolName, 'delete_file')
assert.equal(card.agentName, 'Ops')
assert.equal(card.riskLevel, 'high')
assert.equal(card.riskIcon, '🔥')
assert.equal(card.argumentsJson, '{\n "path": "/tmp/x"\n}')
// Epoch seconds are normalized to an ISO-8601 string for display.
assert.equal(card.createdAt, '2025-09-23T00:00:00.000Z')
})
test('mapApprovalToCardProps falls back for missing/invalid fields', () => {
const card = mapApprovalToCardProps({ id: 'a-2', risk_level: 'nonsense' })
assert.equal(card.toolName, 'unknown')
assert.equal(card.agentName, 'Unknown')
assert.equal(card.riskLevel, 'medium')
assert.equal(card.riskIcon, '⚠️')
assert.equal(card.argumentsJson, '{}')
})
+177
View File
@@ -0,0 +1,177 @@
/**
* PraisonAIUI Agent-UI adapter
*
* Pure mapping functions from wire payloads to stable PraisonAIUI contracts.
* This is the single boundary between transport shapes (WebSocket frames from
* chat.js, REST payloads from /api/approvals/*) and the UI layer. Vendored
* BeautifulUI components are rendered from these contracts elsewhere in
* `src/agent-ui/`; consumers never import upstream prop types directly.
*
* Functions are intentionally side-effect free so they can be unit tested
* with mock events (see adapter.test.ts).
*/
import type {
AgentStreamEvent,
ApprovalPromptModel,
ApprovalRiskLevel,
ToolCallChipModel,
} from './contracts'
/** Raw WebSocket frame as parsed in chat.js (`JSON.parse(event.data)`). */
export type RawChatFrame = Record<string, unknown>
/** Raw approval item as returned by `/api/approvals/pending`. */
export type RawApprovalItem = Record<string, unknown>
const VALID_RISK_LEVELS: readonly ApprovalRiskLevel[] = [
'low',
'medium',
'high',
'critical',
]
function asString(value: unknown): string | undefined {
return typeof value === 'string' ? value : undefined
}
function asNumber(value: unknown): number | undefined {
return typeof value === 'number' ? value : undefined
}
/**
* Normalize a `created_at` value to an ISO-8601 string. The approvals API
* emits epoch seconds (`time.time()`), while some payloads already carry an
* ISO string — accept both, drop anything else.
*/
function asTimestamp(value: unknown): string | undefined {
if (typeof value === 'string') return value
if (typeof value === 'number' && Number.isFinite(value)) {
return new Date(value * 1000).toISOString()
}
return undefined
}
/** Map a raw tool-call frame to a stable chip view model. */
function mapToolCall(
frame: RawChatFrame,
status: ToolCallChipModel['status'],
): ToolCallChipModel {
const name = asString(frame.name) ?? 'tool'
return {
id: asString(frame.tool_call_id) ?? name,
label: asString(frame.description) ?? name,
icon: asString(frame.icon),
stepNumber: asNumber(frame.step_number),
status,
error: asString(frame.error),
}
}
/**
* Map a raw WebSocket chat frame to a normalized {@link AgentStreamEvent}.
*
* Unknown frame types are preserved as `kind: 'unknown'` with `rawType` set,
* so new upstream event types degrade gracefully instead of throwing.
*/
export function mapChatEventToUi(frame: RawChatFrame): AgentStreamEvent {
const rawType = asString(frame.type)
const sessionId = asString(frame.session_id)
const runId = asString(frame.run_id)
const agentName = asString(frame.agent_name)
switch (rawType) {
case 'run_started':
return { kind: 'run-started', sessionId, runId, agentName }
case 'run_content':
case 'team_run_content':
return {
kind: 'run-token',
sessionId,
runId,
agentName,
text: asString(frame.token),
}
case 'run_completed':
case 'team_run_completed':
return {
kind: 'run-completed',
sessionId,
runId,
agentName,
text: asString(frame.content),
}
case 'run_error':
case 'team_run_error':
return {
kind: 'run-error',
sessionId,
runId,
agentName,
error: asString(frame.error) ?? 'Unknown error',
}
case 'tool_call_started':
case 'team_tool_call_started':
return {
kind: 'tool-call',
sessionId,
runId,
agentName,
toolCall: mapToolCall(frame, 'running'),
}
case 'tool_call_completed':
case 'team_tool_call_completed':
return {
kind: 'tool-call',
sessionId,
runId,
agentName,
toolCall: mapToolCall(frame, 'done'),
}
case 'reasoning_step':
case 'team_reasoning_step':
return {
kind: 'reasoning-step',
sessionId,
runId,
agentName,
text: asString(frame.step),
}
default:
return { kind: 'unknown', sessionId, runId, agentName, rawType }
}
}
function normalizeRisk(value: unknown): ApprovalRiskLevel {
return VALID_RISK_LEVELS.includes(value as ApprovalRiskLevel)
? (value as ApprovalRiskLevel)
: 'medium'
}
/**
* Map a raw approval item from `/api/approvals/pending` to a stable
* {@link ApprovalPromptModel}. Arguments are pretty-printed for display and
* an unknown/missing risk level falls back to `medium`.
*/
export function mapApprovalToCardProps(
item: RawApprovalItem,
): ApprovalPromptModel {
let argumentsJson = '{}'
try {
argumentsJson = JSON.stringify(item.arguments ?? {}, null, 2)
} catch {
// Circular or non-serializable args — degrade to empty object.
argumentsJson = '{}'
}
return {
id: asString(item.id) ?? '',
toolName: asString(item.tool_name) ?? 'unknown',
agentName: asString(item.agent_name) ?? 'Unknown',
riskLevel: normalizeRisk(item.risk_level),
riskIcon: asString(item.risk_icon) ?? '⚠️',
description: asString(item.description),
argumentsJson,
createdAt: asTimestamp(item.created_at),
}
}
+69
View File
@@ -0,0 +1,69 @@
/**
* PraisonAIUI Agent-UI contracts
*
* Stable, framework-agnostic view models owned by PraisonAIUI. These are the
* ONLY types that non-adapter code (chat views, dashboards, tests) should
* depend on. BeautifulUI / vendored component prop types must never leak
* outside `src/agent-ui/` — the adapter maps to these contracts instead so
* the upstream implementation can be swapped without touching consumers.
*
* See docs/frontend/beautifului-adapter-architecture.md.
*/
/** Discriminant for a normalized agent stream event. */
export type AgentStreamEventKind =
| 'run-started'
| 'run-token'
| 'run-completed'
| 'run-error'
| 'tool-call'
| 'reasoning-step'
| 'unknown'
/**
* Normalized agent stream event — the stable shape UI code consumes,
* decoupled from the raw WebSocket frame `type` strings handled in chat.js.
*/
export interface AgentStreamEvent {
kind: AgentStreamEventKind
/** Owning session, when the frame carried one. */
sessionId?: string
/** Run identifier, when the frame carried one. */
runId?: string
/** Human-facing agent name, when present. */
agentName?: string
/** Streaming token or final content, depending on `kind`. */
text?: string
/** Populated for `kind === 'tool-call'`. */
toolCall?: ToolCallChipModel
/** Populated for `kind === 'run-error'`. */
error?: string
/** Raw frame `type` for events we do not yet normalize (`kind === 'unknown'`). */
rawType?: string
}
/** View model for a tool-call chip, independent of any component library. */
export interface ToolCallChipModel {
id: string
label: string
icon?: string
stepNumber?: number
status: 'running' | 'done' | 'error'
error?: string
}
/** Risk levels surfaced by the approvals REST API (`/api/approvals/*`). */
export type ApprovalRiskLevel = 'low' | 'medium' | 'high' | 'critical'
/** View model for an approval prompt/card, independent of any component library. */
export interface ApprovalPromptModel {
id: string
toolName: string
agentName: string
riskLevel: ApprovalRiskLevel
riskIcon: string
description?: string
/** Pretty-printed tool arguments, ready for display. */
argumentsJson: string
createdAt?: string
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Agent-UI boundary barrel.
*
* Consumers import PraisonAIUI's stable agent-UI contracts and adapters from
* here. Vendored BeautifulUI components live under `src/agent-ui/vendors/` and
* are re-exported from this folder only — no upstream prop types should be
* imported outside `src/agent-ui/`.
*/
export type {
AgentStreamEvent,
AgentStreamEventKind,
ApprovalPromptModel,
ApprovalRiskLevel,
ToolCallChipModel,
} from './contracts'
export type { RawApprovalItem, RawChatFrame } from './adapter'
export { mapApprovalToCardProps, mapChatEventToUi } from './adapter'
+1 -1
View File
@@ -1,3 +1,3 @@
"""Version information."""
__version__ = "0.3.144"
__version__ = "0.3.147"
+48
View File
@@ -44,6 +44,43 @@ def _icon(symbol: str, fallback: str) -> str:
return symbol if _supports_unicode() else fallback
def _load_env_files() -> int:
"""Load environment variables from the standard PraisonAI ``.env`` files.
Mirrors the PraisonAI CLI convention so ``env:VAR`` references (e.g. in
Channels config) resolve when the server was started without the shell
exporting the tokens. Reads ``~/.praisonai/.env`` first, then a ``.env`` in
the current working directory. Existing process env vars are never
overridden, and parsing is intentionally dependency-free (no python-dotenv).
Returns the number of variables newly set into ``os.environ``.
"""
import os
loaded = 0
for env_path in (Path.home() / ".praisonai" / ".env", Path.cwd() / ".env"):
try:
if not env_path.is_file():
continue
for line in env_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
if line.startswith("export "):
line = line[len("export "):]
key, value = line.split("=", 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
if key and key not in os.environ:
os.environ[key] = value
loaded += 1
except Exception:
# Never let env loading break startup — resolution falls back to
# the existing process environment.
continue
return loaded
app = typer.Typer(
name="aiui",
help="PraisonAIUI - YAML-driven website generator",
@@ -1380,6 +1417,17 @@ def run(
console.print(f"[red]Error:[/red] App file not found: {app_file}")
raise typer.Exit(code=1)
# Auto-load standard PraisonAI env files so `env:VAR` references (e.g. in
# Channels config) resolve even when the shell did not export the tokens.
# This matches the PraisonAI CLI convention and fixes the "Connected but
# bot never replies" class of issues caused by empty env-resolved tokens.
_loaded = _load_env_files()
if _loaded:
console.print(
f"[green]{_icon('✓', 'OK')}[/green] Loaded {_loaded} environment "
f"variable(s) from PraisonAI config"
)
is_yaml = app_file.suffix in (".yaml", ".yml")
if is_yaml:
+13 -2
View File
@@ -652,13 +652,24 @@ class ChannelsFeature(BaseFeatureProtocol):
if hasattr(bot, "_session") and hasattr(bot._session, "chat"):
_original_chat = bot._session.chat
async def _wrapped_chat(agent: Any, user_id: str, text: str) -> str:
async def _wrapped_chat(
agent: Any,
user_id: str,
text: str,
*args: Any,
**kwargs: Any,
) -> str:
"""Wrap agent chat to broadcast responses AND intermediate
tool/step events to the Chat UI.
Hooks into the agent's StreamEventEmitter (same pattern as
PraisonAIProvider._run_direct_mode) to capture tool_call,
reasoning, and content events during execution.
Any extra positional/keyword arguments (e.g. ``chat_id``,
``platform``, ``thread_id``) are forwarded untouched to the
wrapped session ``chat`` so per-conversation memory/session
continuity is preserved instead of raising ``TypeError``.
"""
from .chat import _enrich_tool_payload, get_chat_manager
@@ -831,7 +842,7 @@ class ChannelsFeature(BaseFeatureProtocol):
drain_task = asyncio.create_task(_drain_events())
try:
response = await _original_chat(agent, user_id, text)
response = await _original_chat(agent, user_id, text, *args, **kwargs)
finally:
# Signal drain to stop and clean up callback
try:
@@ -1,65 +1,154 @@
/**
* Approvals View — execution approval queue
* API: /api/approvals
*
* Pending items render as an Approval Card (human-in-the-loop): risk, question,
* optional preset options / custom answer / confidence copy — surfaced only when
* the API supplies them, so the card stays a plain approve/deny prompt otherwise.
*/
import { isSkillApproval } from '/plugins/views/_helpers.js';
import { isSkillApproval, esc, emptyState } from '/plugins/views/_helpers.js';
const RISK = {
low: { icon: '✅', color: '#22c55e' },
medium: { icon: '⚠️', color: '#eab308' },
high: { icon: '🟠', color: '#f97316' },
critical: { icon: '🔴', color: '#ef4444' },
};
/**
* Normalise a raw /api/approvals item into an ApprovalPromptModel-shaped object.
* Keeps the field-mapping in one place; only carries what the API actually sends.
*/
function toApprovalModel(a) {
const id = a.id || a.approval_id || '';
const risk = String(a.risk_level || '').toLowerCase();
const options = Array.isArray(a.options)
? a.options.map(o => (typeof o === 'string' ? { value: o, label: o } : { value: o.value ?? o.id ?? o.label, label: o.label ?? String(o.value ?? o.id) }))
: [];
const confidence = typeof a.confidence === 'number' ? Math.round(a.confidence * (a.confidence <= 1 ? 100 : 1)) : null;
return {
id,
title: a.tool_name || a.action || 'Unknown',
question: a.reason || a.description || a.question || '',
agent: a.agent_name || '',
risk,
riskMeta: RISK[risk] || { icon: '❓', color: 'var(--db-text-dim)' },
createdAt: a.created_at ? new Date(a.created_at * 1000).toLocaleString() : '',
options,
allowCustom: a.allow_custom_answer === true || (options.length === 0 && a.custom_answer !== false && !!a.question),
confidence,
isSkill: isSkillApproval(a),
};
}
function approvalCard(m) {
const optionsHtml = m.options.length
? `<div class="db-approval-options" role="radiogroup" aria-label="Response options" style="display:flex;flex-wrap:wrap;gap:6px;margin-top:10px">
${m.options.map((o, i) => `
<label style="display:inline-flex;align-items:center;gap:6px;font-size:12px;padding:4px 10px;border:1px solid var(--db-border);border-radius:6px;cursor:pointer">
<input type="radio" name="opt-${esc(m.id)}" value="${esc(o.value)}" ${i === 0 ? 'checked' : ''}> ${esc(o.label)}
</label>`).join('')}
</div>`
: '';
const customHtml = m.allowCustom
? `<input class="approv-custom" data-id="${esc(m.id)}" type="text" placeholder="Custom answer (optional)"
aria-label="Custom answer for ${esc(m.title)}"
style="margin-top:10px;width:100%;padding:6px 10px;font-size:12px;background:var(--db-bg);color:var(--db-text);border:1px solid var(--db-border);border-radius:6px">`
: '';
const confidenceHtml = m.confidence != null
? `<div style="font-size:11px;color:var(--db-text-dim);margin-top:6px">Model confidence: ${m.confidence}%</div>`
: '';
const skillHtml = m.isSkill
? `<a href="/skills?tab=pending&approval_id=${encodeURIComponent(m.id)}" data-skill-studio="1" style="display:inline-block;margin-top:8px;font-size:11px;color:var(--db-accent);text-decoration:none">Skill write? Open Skills Studio →</a>`
: '';
return `
<div class="db-card db-approval-card" data-id="${esc(m.id)}" role="group"
aria-label="Approval request: ${esc(m.title)}"
style="margin-bottom:12px;padding:16px 20px;border-left:3px solid ${m.riskMeta.color}">
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px">
<div style="flex:1;min-width:0">
<div style="font-weight:600;font-size:14px">${m.riskMeta.icon} ${esc(m.title)}</div>
${m.question ? `<div style="font-size:13px;color:var(--db-text);margin-top:6px">${esc(m.question)}</div>` : ''}
<div style="font-size:12px;color:var(--db-text-dim);margin-top:4px">Agent: ${esc(m.agent) || '—'}${m.risk ? ` · risk: ${esc(m.risk)}` : ''}</div>
${m.createdAt ? `<div style="font-size:11px;color:var(--db-text-dim)">${esc(m.createdAt)}</div>` : ''}
${optionsHtml}
${customHtml}
${confidenceHtml}
${skillHtml}
</div>
<div style="display:flex;gap:8px;flex-shrink:0">
<button class="approv-yes" data-id="${esc(m.id)}" aria-label="Approve ${esc(m.title)}"
style="padding:6px 16px;background:rgba(34,197,94,0.15);color:#22c55e;border:1px solid rgba(34,197,94,0.3);border-radius:6px;cursor:pointer;font-size:12px">✓ Approve</button>
<button class="approv-no" data-id="${esc(m.id)}" aria-label="Deny ${esc(m.title)}"
style="padding:6px 16px;background:rgba(239,68,68,0.15);color:#ef4444;border:1px solid rgba(239,68,68,0.3);border-radius:6px;cursor:pointer;font-size:12px">✗ Deny</button>
</div>
</div>
</div>`;
}
export async function render(container) {
container.innerHTML = '<div class="db-loading"><div class="db-spinner"></div></div>';
let pending = [], history = [], policies = {};
try { const r = await fetch('/api/approvals/pending'); pending = (await r.json()).approvals || []; } catch(e) {}
try { const r = await fetch('/api/approvals/history'); history = (await r.json()).approvals || (await r.json()).history || []; } catch(e) {}
try { const r = await fetch('/api/approvals/history'); const d = await r.json(); history = d.approvals || d.history || []; } catch(e) {}
try { const r = await fetch('/api/approvals/policies'); policies = await r.json(); } catch(e) {}
const riskIcons = { low: '✅', medium: '⚠️', high: '🟠', critical: '🔴' };
const models = pending.map(toApprovalModel);
container.innerHTML = `
<div class="db-columns" style="grid-template-columns:repeat(3,1fr);margin-bottom:24px">
<div class="db-card"><div class="db-card-title">Pending</div><div class="db-card-value" style="color:#eab308">${pending.length}</div></div>
<div class="db-card"><div class="db-card-title">Risk Threshold</div><div class="db-card-value" style="font-size:18px">${policies.risk_threshold || 'high'}</div></div>
<div class="db-card"><div class="db-card-title">Pending</div><div class="db-card-value" style="color:#eab308">${models.length}</div></div>
<div class="db-card"><div class="db-card-title">Risk Threshold</div><div class="db-card-value" style="font-size:18px">${esc(policies.risk_threshold || 'high')}</div></div>
<div class="db-card"><div class="db-card-title">Auto-Approve Tools</div><div class="db-card-value" style="font-size:18px">${(policies.auto_approve_tools || []).length}</div></div>
</div>
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600">Pending Approvals</h3>
<div id="approv-pending" style="margin-bottom:28px">
${pending.length === 0 ? '<div class="db-viewer"><pre>No pending approvals</pre></div>' : ''}
${models.length === 0 ? emptyState({ icon: '✅', title: 'No pending approvals', body: 'All clear — nothing needs your decision.' }) : models.map(approvalCard).join('')}
</div>
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600">Recent History</h3>
<div id="approv-history"></div>
`;
const pendingEl = container.querySelector('#approv-pending');
pending.forEach(a => {
const card = document.createElement('div');
card.className = 'db-card';
card.style.cssText = 'margin-bottom:10px;padding:16px 20px;display:flex;align-items:center;justify-content:space-between';
const isSkill = isSkillApproval(a);
card.innerHTML = `
<div style="flex:1">
<div style="font-weight:500;font-size:14px">${riskIcons[a.risk_level] || '❓'} ${a.tool_name || a.action || 'Unknown'}</div>
<div style="font-size:12px;color:var(--db-text-dim);margin-top:4px">Agent: ${a.agent_name || '—'} · ${a.reason || ''}</div>
<div style="font-size:11px;color:var(--db-text-dim)">${a.created_at ? new Date(a.created_at * 1000).toLocaleString() : ''}</div>
${isSkill ? `<a href="/skills?tab=pending&approval_id=${encodeURIComponent(a.id || a.approval_id || '')}" data-skill-studio="1" style="font-size:11px;color:var(--db-accent);text-decoration:none">Skill write? Open Skills Studio →</a>` : ''}
</div>
<div style="display:flex;gap:8px">
<button class="approv-yes" data-id="${a.id || a.approval_id}" style="padding:6px 16px;background:rgba(34,197,94,0.15);color:#22c55e;border:1px solid rgba(34,197,94,0.3);border-radius:6px;cursor:pointer;font-size:12px">✓ Approve</button>
<button class="approv-no" data-id="${a.id || a.approval_id}" style="padding:6px 16px;background:rgba(239,68,68,0.15);color:#ef4444;border:1px solid rgba(239,68,68,0.3);border-radius:6px;cursor:pointer;font-size:12px">✗ Deny</button>
</div>
`;
pendingEl.appendChild(card);
});
function collectExtra(id) {
const card = container.querySelector(`.db-approval-card[data-id="${CSS.escape(id)}"]`);
const body = {};
if (!card) return body;
const opt = card.querySelector(`input[name="opt-${CSS.escape(id)}"]:checked`);
if (opt) body.option = opt.value;
const custom = card.querySelector('.approv-custom');
if (custom && custom.value.trim()) body.custom_answer = custom.value.trim();
// Also surface the human's choice as `reason` — the only extra field the
// approve/deny endpoints persist today — so the answer isn't dropped while
// `option`/`custom_answer` stay for a future richer backend.
const reason = body.custom_answer || body.option;
if (reason != null && reason !== '') body.reason = String(reason);
return body;
}
container.querySelectorAll('.approv-yes').forEach(b => b.addEventListener('click', async () => { try { await fetch(`/api/approvals/${b.dataset.id}/approve`, {method:'POST'}); render(container); } catch(e){} }));
container.querySelectorAll('.approv-no').forEach(b => b.addEventListener('click', async () => { try { await fetch(`/api/approvals/${b.dataset.id}/deny`, {method:'POST'}); render(container); } catch(e){} }));
async function decide(id, action) {
try {
await fetch(`/api/approvals/${encodeURIComponent(id)}/${action}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(collectExtra(id)),
});
render(container);
} catch(e) {}
}
container.querySelectorAll('.approv-yes').forEach(b => b.addEventListener('click', () => decide(b.dataset.id, 'approve')));
container.querySelectorAll('.approv-no').forEach(b => b.addEventListener('click', () => decide(b.dataset.id, 'deny')));
const historyEl = container.querySelector('#approv-history');
(Array.isArray(history) ? history : []).slice(0, 20).forEach(h => {
const div = document.createElement('div');
div.style.cssText = 'padding:8px 0;border-bottom:1px solid var(--db-border);font-size:13px;display:flex;justify-content:space-between';
const approved = h.status === 'approved' || h.approved;
div.innerHTML = `<span>${h.tool_name || h.action || '?'} — ${h.agent_name || ''}</span><span style="color:${approved ? '#22c55e' : '#ef4444'}">${approved ? '✓ approved' : '✗ denied'}</span>`;
div.innerHTML = `<span>${esc(h.tool_name || h.action || '?')} — ${esc(h.agent_name || '')}</span><span style="color:${approved ? '#22c55e' : '#ef4444'}">${approved ? '✓ approved' : '✗ denied'}</span>`;
historyEl.appendChild(div);
});
if (!history.length) historyEl.innerHTML = '<div style="font-size:13px;color:var(--db-text-dim)">No history</div>';
+88
View File
@@ -0,0 +1,88 @@
"""Regression coverage for issue #287: the channels chat bridge dropped
extra keyword arguments (e.g. ``chat_id``, ``platform``) because the
``_wrapped_chat`` wrapper installed over ``bot._session.chat`` had a fixed
``(agent, user_id, text)`` signature.
When the bot internals invoked the session with per-conversation kwargs the
wrapper raised ``TypeError: _wrapped_chat() got an unexpected keyword
argument 'chat_id'`` and the bot silently failed to reply. The wrapper must
forward any extra positional/keyword arguments untouched to the original
session ``chat`` so memory/session continuity is preserved.
"""
from __future__ import annotations
from typing import Any
import pytest
from praisonaiui.features.channels import ChannelsFeature
class _FakeSession:
"""Minimal session whose ``chat`` records forwarded arguments."""
def __init__(self) -> None:
self.received_args: tuple = ()
self.received_kwargs: dict = {}
async def chat(self, agent: Any, user_id: str, text: str, *args: Any, **kwargs: Any) -> str:
self.received_args = args
self.received_kwargs = dict(kwargs)
return f"reply to {text}"
class _FakeBot:
def __init__(self) -> None:
self._session = _FakeSession()
@pytest.mark.asyncio
async def test_wrapped_chat_forwards_kwargs() -> None:
feature = ChannelsFeature()
bot = _FakeBot()
feature._attach_chat_bridge("chan-1", bot, "telegram")
# The bridge replaced the session chat with the wrapper.
assert bot._session.chat.__name__ == "_wrapped_chat"
reply = await bot._session.chat(
object(),
"user-42",
"Hi",
chat_id="987654321",
platform="telegram",
user_id_meta="987654321",
)
assert reply == "reply to Hi"
assert bot._session.received_kwargs["chat_id"] == "987654321"
assert bot._session.received_kwargs["platform"] == "telegram"
@pytest.mark.asyncio
async def test_wrapped_chat_forwards_extra_positional_args() -> None:
feature = ChannelsFeature()
bot = _FakeBot()
feature._attach_chat_bridge("chan-2", bot, "slack")
reply = await bot._session.chat(object(), "user-7", "Hello", "extra")
assert reply == "reply to Hello"
assert bot._session.received_args == ("extra",)
@pytest.mark.asyncio
async def test_wrapped_chat_still_works_without_kwargs() -> None:
"""No-kwargs path (web UI / existing callers) must keep working."""
feature = ChannelsFeature()
bot = _FakeBot()
feature._attach_chat_bridge("chan-3", bot, "discord")
reply = await bot._session.chat(object(), "user-9", "Ping")
assert reply == "reply to Ping"
assert bot._session.received_kwargs == {}
+68
View File
@@ -0,0 +1,68 @@
"""Unit tests for `_load_env_files` — auto-loading PraisonAI `.env` files.
Regression coverage for issue #286: `env:VAR` channel token references
resolved to empty strings because `aiui run` never loaded the standard
`~/.praisonai/.env` file, so bots showed "Connected" but never replied.
"""
import os
from praisonaiui.cli import _load_env_files
def _write_env(path, body):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(body, encoding="utf-8")
def test_loads_home_praisonai_env(tmp_path, monkeypatch):
"""Variables in ~/.praisonai/.env are loaded into the process env."""
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
monkeypatch.chdir(tmp_path)
monkeypatch.delenv("TELEGRAM_BOT_TOKEN", raising=False)
_write_env(tmp_path / ".praisonai" / ".env", 'TELEGRAM_BOT_TOKEN="123:AAtoken"\n')
loaded = _load_env_files()
assert loaded >= 1
assert os.environ["TELEGRAM_BOT_TOKEN"] == "123:AAtoken"
def test_does_not_override_existing_env(tmp_path, monkeypatch):
"""An already-set process variable is never clobbered by the file."""
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("SLACK_BOT_TOKEN", "xoxb-from-shell")
_write_env(tmp_path / ".praisonai" / ".env", "SLACK_BOT_TOKEN=xoxb-from-file\n")
_load_env_files()
assert os.environ["SLACK_BOT_TOKEN"] == "xoxb-from-shell"
def test_ignores_comments_blanks_and_export_prefix(tmp_path, monkeypatch):
"""Comments/blank lines are skipped and `export ` prefixes are stripped."""
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
monkeypatch.chdir(tmp_path)
monkeypatch.delenv("FOO", raising=False)
monkeypatch.delenv("BAR", raising=False)
_write_env(
tmp_path / ".praisonai" / ".env",
"# a comment\n\nexport FOO=foo\nBAR='bar'\n",
)
_load_env_files()
assert os.environ["FOO"] == "foo"
assert os.environ["BAR"] == "bar"
def test_missing_files_are_harmless(tmp_path, monkeypatch):
"""No env files present → returns 0 and does not raise."""
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
monkeypatch.chdir(tmp_path)
assert _load_env_files() == 0
+20
View File
@@ -478,6 +478,26 @@ def test_voice_doctor_report(monkeypatch):
assert report["summary"]["failed"] >= 1
def test_voice_doctor_mcp_probe_timeout_is_warn(monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "sk-test-key-12345678")
def fake_http_status(url: str, timeout: float = 5.0):
if "/api/mcp/servers" in url:
assert timeout == 2.0
return "fail", "timed out"
return "pass", "HTTP 200"
with _voice_modules() as imp:
doctor = imp("integrations.voice.doctor")
monkeypatch.setattr(doctor, "_tcp_open", lambda *args, **kwargs: True)
monkeypatch.setattr(doctor, "_http_status", fake_http_status)
report = doctor.run_voice_doctor(app_port=8001, sidecar_port=59998)
mcp = next(c for c in report["checks"] if c["name"] == "MCP servers API")
assert mcp["status"] == "warn"
assert mcp["detail"] == "timed out"
def test_call_detail_ui_includes_analytics():
with _voice_modules() as imp:
ui = imp("integrations.voice.call_detail_ui")
Generated
+92 -6
View File
@@ -207,7 +207,7 @@ wheels = [
[[package]]
name = "aiui"
version = "0.3.144"
version = "0.3.147"
source = { editable = "." }
dependencies = [
{ name = "pydantic" },
@@ -1241,7 +1241,8 @@ dependencies = [
{ name = "mmh3" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "onnxruntime" },
{ name = "onnxruntime", version = "1.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "onnxruntime", version = "1.24.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-grpc" },
{ name = "opentelemetry-sdk" },
@@ -1311,6 +1312,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "coloredlogs"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "humanfriendly" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" },
]
[[package]]
name = "coverage"
version = "7.13.2"
@@ -2450,6 +2463,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/92/e3/e3a44f54c8e2f28983fcf07f13d4260b37bd6a0d3a081041bc60b91d230e/huggingface_hub-1.6.0-py3-none-any.whl", hash = "sha256:ef40e2d5cb85e48b2c067020fa5142168342d5108a1b267478ed384ecbf18961", size = 612874, upload-time = "2026-03-06T14:19:16.844Z" },
]
[[package]]
name = "humanfriendly"
version = "10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyreadline3", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" },
]
[[package]]
name = "humanize"
version = "4.15.0"
@@ -3355,7 +3380,8 @@ dependencies = [
{ name = "click" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "onnxruntime" },
{ name = "onnxruntime", version = "1.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "onnxruntime", version = "1.24.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "python-dotenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/8fdd991142ad3e037179a494b153f463024e5a211ef3ad948b955c26b4de/magika-0.6.2.tar.gz", hash = "sha256:37eb6ae8020f6e68f231bc06052c0a0cbe8e6fa27492db345e8dc867dbceb067", size = 3036634, upload-time = "2025-05-02T14:54:18.88Z" }
@@ -3379,7 +3405,8 @@ dependencies = [
{ name = "click" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "onnxruntime" },
{ name = "onnxruntime", version = "1.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "onnxruntime", version = "1.24.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "python-dotenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/f3/3d1dcdd7b9c41d589f5cff252d32ed91cdf86ba84391cfc81d9d8773571d/magika-0.6.3.tar.gz", hash = "sha256:7cc52aa7359af861957043e2bf7265ed4741067251c104532765cd668c0c0cb1", size = 3042784, upload-time = "2025-10-30T15:22:34.499Z" }
@@ -4292,14 +4319,64 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565, upload-time = "2023-12-01T16:22:51.518Z" },
]
[[package]]
name = "onnxruntime"
version = "1.23.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11' and sys_platform == 'win32'",
"python_full_version < '3.11' and sys_platform != 'win32'",
]
dependencies = [
{ name = "coloredlogs" },
{ name = "flatbuffers" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
{ name = "packaging" },
{ name = "protobuf" },
{ name = "sympy" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/d6/311b1afea060015b56c742f3531168c1644650767f27ef40062569960587/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3", size = 17195934, upload-time = "2025-10-27T23:06:14.143Z" },
{ url = "https://files.pythonhosted.org/packages/db/db/81bf3d7cecfbfed9092b6b4052e857a769d62ed90561b410014e0aae18db/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:b28740f4ecef1738ea8f807461dd541b8287d5650b5be33bca7b474e3cbd1f36", size = 19153079, upload-time = "2025-10-27T23:05:57.686Z" },
{ url = "https://files.pythonhosted.org/packages/2e/4d/a382452b17cf70a2313153c520ea4c96ab670c996cb3a95cc5d5ac7bfdac/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f7d1fe034090a1e371b7f3ca9d3ccae2fabae8c1d8844fb7371d1ea38e8e8d2", size = 15219883, upload-time = "2025-10-22T03:46:21.66Z" },
{ url = "https://files.pythonhosted.org/packages/fb/56/179bf90679984c85b417664c26aae4f427cba7514bd2d65c43b181b7b08b/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ca88747e708e5c67337b0f65eed4b7d0dd70d22ac332038c9fc4635760018f7", size = 17370357, upload-time = "2025-10-22T03:46:57.968Z" },
{ url = "https://files.pythonhosted.org/packages/cd/6d/738e50c47c2fd285b1e6c8083f15dac1a5f6199213378a5f14092497296d/onnxruntime-1.23.2-cp310-cp310-win_amd64.whl", hash = "sha256:0be6a37a45e6719db5120e9986fcd30ea205ac8103fd1fb74b6c33348327a0cc", size = 13467651, upload-time = "2025-10-27T23:06:11.904Z" },
{ url = "https://files.pythonhosted.org/packages/44/be/467b00f09061572f022ffd17e49e49e5a7a789056bad95b54dfd3bee73ff/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c", size = 17196113, upload-time = "2025-10-22T03:47:33.526Z" },
{ url = "https://files.pythonhosted.org/packages/9f/a8/3c23a8f75f93122d2b3410bfb74d06d0f8da4ac663185f91866b03f7da1b/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612", size = 19153857, upload-time = "2025-10-22T03:46:37.578Z" },
{ url = "https://files.pythonhosted.org/packages/3f/d8/506eed9af03d86f8db4880a4c47cd0dffee973ef7e4f4cff9f1d4bcf7d22/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6", size = 15220095, upload-time = "2025-10-22T03:46:24.769Z" },
{ url = "https://files.pythonhosted.org/packages/e9/80/113381ba832d5e777accedc6cb41d10f9eca82321ae31ebb6bcede530cea/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e", size = 17372080, upload-time = "2025-10-22T03:47:00.265Z" },
{ url = "https://files.pythonhosted.org/packages/3a/db/1b4a62e23183a0c3fe441782462c0ede9a2a65c6bbffb9582fab7c7a0d38/onnxruntime-1.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e", size = 13468349, upload-time = "2025-10-22T03:47:25.783Z" },
{ url = "https://files.pythonhosted.org/packages/1b/9e/f748cd64161213adeef83d0cb16cb8ace1e62fa501033acdd9f9341fff57/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321", size = 17195929, upload-time = "2025-10-22T03:47:36.24Z" },
{ url = "https://files.pythonhosted.org/packages/91/9d/a81aafd899b900101988ead7fb14974c8a58695338ab6a0f3d6b0100f30b/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b", size = 19157705, upload-time = "2025-10-22T03:46:40.415Z" },
{ url = "https://files.pythonhosted.org/packages/3c/35/4e40f2fba272a6698d62be2cd21ddc3675edfc1a4b9ddefcc4648f115315/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c", size = 15226915, upload-time = "2025-10-22T03:46:27.773Z" },
{ url = "https://files.pythonhosted.org/packages/ef/88/9cc25d2bafe6bc0d4d3c1db3ade98196d5b355c0b273e6a5dc09c5d5d0d5/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77", size = 17382649, upload-time = "2025-10-22T03:47:02.782Z" },
{ url = "https://files.pythonhosted.org/packages/c0/b4/569d298f9fc4d286c11c45e85d9ffa9e877af12ace98af8cab52396e8f46/onnxruntime-1.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435", size = 13470528, upload-time = "2025-10-22T03:47:28.106Z" },
{ url = "https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f", size = 17196337, upload-time = "2025-10-22T03:46:35.168Z" },
{ url = "https://files.pythonhosted.org/packages/fe/f9/2d49ca491c6a986acce9f1d1d5fc2099108958cc1710c28e89a032c9cfe9/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95", size = 19157691, upload-time = "2025-10-22T03:46:43.518Z" },
{ url = "https://files.pythonhosted.org/packages/1c/a1/428ee29c6eaf09a6f6be56f836213f104618fb35ac6cc586ff0f477263eb/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b", size = 15226898, upload-time = "2025-10-22T03:46:30.039Z" },
{ url = "https://files.pythonhosted.org/packages/f2/2b/b57c8a2466a3126dbe0a792f56ad7290949b02f47b86216cd47d857e4b77/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872", size = 17382518, upload-time = "2025-10-22T03:47:05.407Z" },
{ url = "https://files.pythonhosted.org/packages/4a/93/aba75358133b3a941d736816dd392f687e7eab77215a6e429879080b76b6/onnxruntime-1.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088", size = 13470276, upload-time = "2025-10-22T03:47:31.193Z" },
{ url = "https://files.pythonhosted.org/packages/7c/3d/6830fa61c69ca8e905f237001dbfc01689a4e4ab06147020a4518318881f/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466", size = 15229610, upload-time = "2025-10-22T03:46:32.239Z" },
{ url = "https://files.pythonhosted.org/packages/b6/ca/862b1e7a639460f0ca25fd5b6135fb42cf9deea86d398a92e44dfda2279d/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145", size = 17394184, upload-time = "2025-10-22T03:47:08.127Z" },
]
[[package]]
name = "onnxruntime"
version = "1.24.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.14' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform != 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform != 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform != 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform != 'win32'",
]
dependencies = [
{ name = "flatbuffers" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" } },
{ name = "packaging" },
{ name = "protobuf" },
{ name = "sympy" },
@@ -5779,6 +5856,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" },
]
[[package]]
name = "pyreadline3"
version = "3.5.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/6d/f94028646d7bbe6d9d873c47ee7c246f2d29129d253f0d96cb6fcab70733/pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf", size = 100368, upload-time = "2026-05-14T17:55:04.471Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d", size = 85243, upload-time = "2026-05-14T17:55:03.262Z" },
]
[[package]]
name = "pytest"
version = "9.0.2"