Compare commits

...
Author SHA1 Message Date
MervinPraisonandClaude Opus 4.8 9ac4414901 fix: register chat runs in _active_runs so abort works (fixes #274)
_run_and_broadcast never inserted its task into ChatManager._active_runs
and never emitted run_started, so the Stop button, POST /api/chat/abort,
and the chat_abort WS message always returned no_active_run while tokens
were still streaming.

- Register the running task under run_id at run start
- Broadcast run_started up-front so the frontend captures run_id and can
  enable the Stop button before the first token
- Deregister the run in a finally on completion, error, or abort

Existing CancelledError handler already broadcasts run_cancelled once the
task is actually cancelled.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-14 09:51:06 +00:00
Mervin Praison 597280a6c6 Merge pull request #265 from Dhivya-Bharathy/feat/meeting-agent-phase-2-recall
feat(meeting-agent): Phase 2 Recall.ai live bots (Part A)
2026-09-08 16:39:01 +01:00
MervinPraisonandClaude Opus 4.8 fe954cbd4a fix(meeting-agent): drop leftover hardcoded Recall workspace UUID from part_b test
The personal contributor workspace UUID (33a035c7-...) was still hardcoded
in the test_meeting_agent_part_b.py env fixture, even though the same value
was cleaned to a neutral placeholder everywhere else (config default,
.env.example, smoke_recall.py, and test_meeting_agent_recall.py). Aligned it
with the sibling test's 'test-workspace' placeholder. No assertion depends on
the value, so behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-08 09:43:30 +00:00
MervinPraisonandClaude Opus 4.8 ab9ef2544d fix(meeting-agent): point Part B tests at renamed meeting_integrations dir
The Part A branch renamed integrations/recall -> meeting_integrations/recall
to avoid a tests/unit/integrations module-name collision, but the Part B test
loaders (test_meeting_agent_part_b.py) still resolved the old integrations/recall
paths, raising FileNotFoundError on CI (py3.10/3.11/3.12). Repoint the 9 _load()
paths at meeting_integrations/recall to match the source layout and the already-
updated test_meeting_agent_recall.py.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-08 09:39:03 +00:00
MervinPraisonandCursor e15b3c0718 merge: resolve conflicts with main (Part B + voice-agent merged)
Reconcile meeting-agent Phase A with main after #266/#267: keep
meeting_integrations/ package rename, merge Part B live-transcript and
Calendar V2 routes, and preserve Part A fixes (atomic webhook idempotency,
background task refs, workspace ID placeholders, py3.10 compiler compat).
Update part_b tests to meeting_integrations paths.

Co-authored-by: Cursor <[email protected]>
2026-09-08 10:34:47 +01:00
MervinPraison 6ecb80e977 Release v0.3.143 2026-09-08 09:05:53 +00:00
Mervin Praison 7ebd97d359 Merge pull request #266 from Dhivya-Bharathy/feat/meeting-agent-phase-2-part-b
feat(meeting-agent): Phase 2 Part B — live transcript + Calendar V2
2026-09-08 10:04:46 +01:00
Mervin Praison bd6b67f38b Merge pull request #267 from Dhivya-Bharathy/feat/voice-agent-phase-1
feat(voice-agent): Phase 1 telephony webhooks, tools, and call dashboard
2026-09-08 10:03:17 +01:00
MervinPraisonandClaude Opus 4.8 442e2d6dfe fix(meeting-agent): retain strong refs to webhook background tasks
asyncio keeps only weak references to tasks created via create_task, so a
fire-and-forget webhook dispatch could be garbage-collected mid-execution and
silently drop the event (a transcript line persist or the post-transcript
pipeline). Route both Recall and calendar webhook dispatches through a helper
that holds a strong reference in a module-level set until the task completes.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-08 08:17:46 +00:00
MervinPraisonandClaude Opus 4.8 3608d011c6 fix(voice-agent): build stable webhook idempotency keys so status updates aren't dropped
webhook_event_key derived the dedup key from a 200-char JSON prefix for
status-update/conversation-update/end-of-call-report. When a large static
field (e.g. `call`) sorts before the discriminating field, successive
events collapse to the same prefix and later ones are dropped as
duplicates, freezing the dashboard status on the first value (e.g. stuck
on 'ringing'). Derive keys from the fields that actually distinguish each
event, matching the existing transcript-event pattern. No new deps,
params, or exports.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 23:45:23 +00:00
MervinPraisonandClaude Opus 4.8 d615e2e0a3 fix(tests): warm up liveness endpoint before timing to fix flaky 3.10 CI
test_health_live_is_fast timed the first request through the Starlette
TestClient, which pays one-time route/client initialization cost and
measured 0.15s on a slow Python 3.10 runner (threshold 0.1s). Add a
warmup request so the assertion measures steady-state latency, matching
the endpoint's own <500ms target. No production code change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 19:26:06 +00:00
MervinPraison 352023db8d fix(meeting-agent): serialize live-transcript metadata read-modify-write
Recall webhooks are dispatched one-thread-each via asyncio.to_thread, so
rapid-fire transcript.data events ran _upsert_live_transcript concurrently.
Each did an unsynchronized read (get_meeting) -> append -> UPDATE of the
persisted meeting transcript, so two concurrent captions could read the same
base and one write would clobber the other, dropping transcript lines.

Add a shared reentrant lock (pipeline._metadata_lock) around every metadata
read-modify-write in merge_meeting_metadata, and hold it across the read in
_upsert_live_transcript so the whole read-append-write is atomic. In-memory
fan-out (live.py) already had a lock; this closes the same gap for the
persisted transcript. Example-only, no package params/exports/deps added.
2026-09-07 19:23:01 +00:00
MervinPraisonandClaude Opus 4.8 bfb1eeee06 fix(voice-agent): preserve live call status on transcript/conversation events
upsert_call overwrote status with excluded.status and defaulted to
'unknown', so transcript and conversation-update events (which pass no
status) reset an in-progress call to 'unknown' in the dashboard. Preserve
the prior status when the caller omits one, matching the existing
COALESCE handling for summary/customer_number. Adds a regression test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 19:21:10 +00:00
MervinPraisonandClaude Opus 4.8 12b42e88be fix(meeting-agent): make Recall webhook idempotency check atomic
Replace the non-atomic SELECT-then-INSERT in mark_event_processed with an
atomic INSERT OR IGNORE + rowcount check. Under concurrent duplicate webhook
deliveries (Svix retries/fan-out) both callers could pass the SELECT and the
second INSERT would raise an unhandled IntegrityError on the primary key,
silently dropping that event's processing after a 200 was returned.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 15:29:26 +00:00
MervinPraisonandClaude Opus 4.8 5e80f11535 fix(meeting-agent): drop hardcoded personal Recall workspace ID from example
The RECALL_WORKSPACE_ID default was a real contributor workspace UUID
committed across the example (config default, README, .env.example, smoke
script, test). It is non-functional — the Recall API key already scopes
requests to a workspace and the value is only echoed back by the
config-status endpoint. Default it to an empty string and replace the
committed UUID with neutral placeholders.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 15:25:58 +00:00
MervinPraisonandClaude Opus 4.8 84ae1fd0ac fix(meeting-agent): retain webhook background task refs to prevent GC
asyncio only holds weak references to tasks created via create_task, so the
fire-and-forget Recall webhook processing task could be garbage-collected
mid-execution, silently dropping transcript/status updates. Keep strong refs
in a module-level set and discard on completion.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 15:21:44 +00:00
MervinPraisonandClaude Opus 4.8 b59af41d88 fix(compiler): use timezone.utc for Python 3.10 compatibility
datetime.UTC was added in Python 3.11, so importing it broke the
python (3.10) CI job with ImportError in _format_lastmod. Switch to
timezone.utc which is equivalent and available on all supported
Python versions (3.10+).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 15:17:41 +00:00
MervinPraisonandClaude Opus 4.8 fb999aba3c fix(seo): use timezone.utc for Python 3.10 compatibility in sitemap lastmod
datetime.UTC was added in Python 3.11, but requires-python is >=3.10, so
Compiler._format_lastmod raised ImportError on 3.10, failing the SEO build
tests and blocking CI. timezone.utc is equivalent and available since 3.2.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 15:15:55 +00:00
MervinPraisonandClaude Opus 4.8 8e5c4d40e2 fix(meeting-agent): resolve CI failures on Phase 2 Recall PR
- Rename example package integrations/ -> meeting_integrations/ to avoid a
  top-level module-name collision with tests/unit/integrations (imported as
  top-level 'integrations' because tests/unit has no __init__.py). The
  collision made 'meeting_integrations.recall' unresolvable once the test
  session imported the tests integrations package first, causing 8 errors +
  3 failures. Updated all absolute imports and test path references.
- Guard praisonai_tools-dependent tests with pytest.importorskip: the meeting
  tools live in the external PraisonAI-Tools package and are not installed in
  this repo's CI, so those tests skip cleanly instead of erroring.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 15:15:01 +00:00
MervinPraisonandClaude Opus 4.8 c7dc8a751a fix(tests): skip meeting-agent tests when praisonai_tools is unavailable
The 6 new meeting-agent tests invoke the external PraisonAI-Tools
'meeting_tools' module, which is not a PraisonAIUI CI dependency, causing
ModuleNotFoundError failures on Python 3.10/3.11/3.12. Guard them with
pytest.importorskip so CI skips them cleanly instead of failing, matching
how the example treats its other optional heavy deps.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 15:11:50 +00:00
MervinPraisonandClaude Opus 4.8 b5b640a2b7 fix(compiler): use timezone.utc for lastmod so Python 3.10 CI passes
datetime.UTC was added in Python 3.11, but the project supports >=3.10
(CI matrix + classifiers), so _format_lastmod raised ImportError on 3.10:
  cannot import name 'UTC' from 'datetime'
Switch to timezone.utc (available since 3.2) — identical behaviour, all
supported versions. Fixes the failing python (3.10) job on PR #267.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 10:21:44 +00:00
MervinPraisonandClaude Opus 4.8 a188039480 fix(voice-agent): hold strong ref to background webhook task to prevent GC drop
asyncio only keeps a weak reference to tasks created via create_task, so a
fire-and-forget webhook-processing task could be garbage-collected mid-flight
and silently drop a status/transcript event. Track tasks in a set and discard
on completion (standard asyncio pattern).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 10:16:51 +00:00
MervinPraisonandClaude Opus 4.8 c71ad7d6da fix(voice-agent): isolate example package import in unit tests to fix CI
tests/unit/integrations ships a top-level 'integrations' package, which
shadowed the voice-agent example's integrations.voice package when the
voice test ran after the integration tests (ModuleNotFoundError: No module
named 'integrations.voice'), failing python 3.10/3.11/3.12 CI.

Import the example's modules inside a context manager that gives the
example dir priority on sys.path and restores the original 'integrations'
package afterwards. Example code is unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 10:13:46 +00:00
MervinPraisonandClaude Opus 4.8 ecf2118bb2 fix(voice-agent): remove committed run logs, timing-safe webhook verify, lazy httpx
- Delete accidentally committed .app.err / .tunnel.log.err (leaked local
  paths, Cloudflare tunnel URLs, connector IDs); ignore *.err
- verify.py: use hmac.compare_digest for constant-time secret comparison
- client.py: lazy-import httpx (optional dep) per AGENTS.md lazy-import rule
- Fix import ordering (ruff I001) in app.py / voice_routes.py

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 10:06:59 +00:00
Mervin Praison 43722c4901 Merge pull request #271 from MervinPraison/chore/hub-v1.1.6
chore(ci): hub v1.1.6 — fix Claude Assistant hub checkout
2026-09-07 11:02:21 +01:00
MervinPraisonandCursor 798e893153 chore(ci): bump hub to v1.1.6 (claude-code-action checkout fix)
Co-authored-by: Cursor <[email protected]>
2026-09-07 11:02:16 +01:00
Mervin Praison 696455db7b Merge pull request #270 from MervinPraison/chore/hub-v1.1.5
fix(ci): hub v1.1.5 Claude Assistant caller permissions
2026-09-07 11:00:42 +01:00
MervinPraisonandCursor 9735e1c4af fix(ci): hub v1.1.5 — Claude Assistant caller permissions
Co-authored-by: Cursor <[email protected]>
2026-09-07 11:00:33 +01:00
Mervin Praison effc3982d6 Merge pull request #269 from MervinPraison/chore/hub-v1.1.4
fix(ci): hub v1.1.4 caller permissions for Claude review chain
2026-09-07 10:59:06 +01:00
MervinPraisonandCursor e67075b799 fix(ci): hub v1.1.4 — grant reusable-workflow caller permissions
Repos with default_workflow_permissions: read reject Auto PR Comment and merge
gate when the caller only grants pull-requests: read but callee jobs need write.

Co-authored-by: Cursor <[email protected]>
2026-09-07 10:58:56 +01:00
Mervin Praison 993114bb55 Merge pull request #268 from MervinPraison/chore/hub-v1.1.3
chore(ci): bump github-automation hub to v1.1.3
2026-09-07 10:53:23 +01:00
MervinPraisonandCursor 8c59d2db85 chore(ci): bump github-automation hub to v1.1.3
Fixes broken reusable workflow refs (v1.1.1 dynamic hub-setup) and review-chain
callable YAML so Auto PR Comment / merge gate / CI-failure Claude run again.

Co-authored-by: Cursor <[email protected]>
2026-09-07 10:53:05 +01:00
Dhivya-Bharathy dc98a79f6e feat(voice-agent): Phase 1 telephony webhooks, tools, and call dashboard 2026-09-07 13:18:37 +05:30
Dhivya-BharathyandCursor ced5a52871 feat(meeting-agent): Phase 2 Part B — live transcript + Calendar V2
Co-authored-by: Cursor <[email protected]>
2026-09-04 17:55:33 +05:30
Dhivya-BharathyandCursor 5f3b026da8 feat(meeting-agent): Phase 2 Recall.ai live bots (Part A)
Add Recall integration for live Google Meet/Zoom/Teams capture: bot scheduling, verified webhooks, async transcription, and post-call summarize + index pipeline. Includes recovery script, smoke mode, and unit tests. Phase 1 upload flow unchanged.

Co-authored-by: Cursor <[email protected]>
2026-09-04 10:59:01 +05:30
MervinPraisonandCursor 15aa219cb3 chore(automation): upgrade to hub v1.1.1 and fix pre-labelled issue Claude trigger
Switch merge-gate, review chain, CI failure, and pipeline workflows to thin
hub callers. Fix auto-issue-comment and claude-dedupe so bot-routed issues
opened with the claude label still dispatch Claude.

Co-authored-by: Cursor <[email protected]>
2026-09-02 13:08:41 +01:00
Mervin Praison 5c88692009 Merge pull request #261 from MervinPraison/claude/issue-260-20260902-meeting-ui
feat: thin meeting-agent UI example (PAI-MEET-UI)
2026-09-02 10:33:55 +01:00
praisonai-triage-agent[bot]andMervin Praison 83c273eba3 fix(compiler): remove extraneous f-string prefix breaking ruff (CI)
CI job `python (3.12)` failed at `ruff check src/praisonaiui` with F541
(f-string without placeholders) on compiler.py:922, blocking pytest before
it ran. Removed the unnecessary `f` prefix on the static og:type meta tag.

Co-authored-by: Mervin Praison <[email protected]>
2026-09-02 09:31:10 +00:00
MervinPraisonandClaude ee44c38297 feat: add thin meeting-agent UI example composing existing features (fixes #260)
Adds examples/python/meeting-agent/ wiring existing attachments, knowledge,
chat and Slack features via YAML/config only. No new package, no new public
exports, no create_app()/server.py params, no eager heavy deps.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-09-02 09:26:05 +00:00
Mervin Praison 8a79913cc4 Merge pull request #228 from MervinPraison/claude/issue-222-20260715-1318
feat: Training Lab UI for praisonai-train agent sessions (STITCH-012)
2026-09-02 10:09:12 +01:00
Mervin Praison af97f31e68 Merge pull request #229 from MervinPraison/claude/issue-217-20260715-run-recorder
feat: Run Flight Recorder — /runs three-pane run debugger (#217)
2026-09-02 10:09:00 +01:00
MervinPraisonandCursor 812c53abaa fix(docs): prevent stale content and full reloads on fast navigation
Abort in-flight markdown fetches, clear the article immediately on route change, and always intercept sidebar links so rapid clicks stay in the SPA instead of showing the wrong page.

Co-authored-by: Cursor <[email protected]>
2026-08-28 12:54:38 +01:00
MervinPraisonandCursor 4bc7497e99 feat(docs): Mintlify-style full-width layout and header navigation
Adopt a three-column docs shell with a centred 720px content column, integrated header tabs, search, and tab-filtered sidebar so navigation matches Mintlify-style sites.

Co-authored-by: Cursor <[email protected]>
2026-08-28 11:16:35 +01:00
MervinPraisonandCursor 4bf74be9c7 fix(docs): render Mermaid diagrams full width like Mintlify
Use useMaxWidth, measure the container width on render/resize, and scale
SVG to 100% of the content column. Widen the docs main area by removing
max-w-3xl so diagrams use space between sidebar and TOC.

Co-authored-by: Cursor <[email protected]>
2026-08-28 10:23:12 +01:00
MervinPraisonandCursor 800883aaf4 test: assert mermaid renders via code component unwrap
Co-authored-by: Cursor <[email protected]>
2026-08-27 14:58:06 +01:00
MervinPraisonandCursor fc28c77e84 fix(docs): detect mermaid fences in code component
Route ```mermaid blocks through the code markdown handler so diagrams
render reliably across react-markdown versions, with pre unwrapping the
MermaidDiagram output.

Co-authored-by: Cursor <[email protected]>
2026-08-27 14:47:03 +01:00
MervinPraisonandCursor 82980cee1a fix(docs): render Mermaid diagrams in React SPA
The mermaid plugin skips React-managed article content to avoid DOM
reconciler crashes, so fenced mermaid blocks showed as raw code. Add a
native MermaidDiagram component with lazy-loaded mermaid@11, theme sync,
and bundled frontend assets.

Co-authored-by: Cursor <[email protected]>
2026-08-27 14:37:34 +01:00
MervinPraisonandCursor 6bfc19a0ee fix(ci): use force_orphan for reliable GitHub Pages deploys
peaceiris was rm-ing gh-pages files then skipping commit when the tree
matched HEAD, leaving llms.txt and sitemap offline. force_orphan replaces
the branch on every deploy.

Co-authored-by: Cursor <[email protected]>
2026-08-26 22:28:25 +01:00
MervinPraisonandCursor 6f855aee3e feat(seo): complete crawl readiness — llms-full, lastmod, auto descriptions
Add llms-full.txt, sitemap lastmod dates, auto meta descriptions from first
paragraph, per-page noindex support, runtime JSON-LD/OG/Twitter updates, a
dedicated Deploy Docs workflow, and corrected SEO documentation.

Co-authored-by: Cursor <[email protected]>
2026-08-26 22:22:09 +01:00
MervinPraisonandCursor 203c57710a ci: allow manual workflow dispatch for docs deploy
Push webhooks stopped firing CI after 13:32 UTC; workflow_dispatch
lets us redeploy GitHub Pages with sitemap, robots.txt, and llms.txt.

Co-authored-by: Cursor <[email protected]>
2026-08-26 17:21:52 +01:00
MervinPraisonandCursor bd6d08b08e feat(seo): add llms.txt, crawl nav, and homepage sitemap entry
Generate llms.txt for AI crawlers, inject static nav links into prerendered
HTML, convert markdown links in noscript content, and include the site root
in sitemap.xml so Google can discover all doc pages once deployed.

Co-authored-by: Cursor <[email protected]>
2026-08-26 17:20:59 +01:00
MervinPraisonandCursor 90feb197b2 fix(docs): stop plugin DOM mutations that crash React SPA navigation
Guard plugin-loader, syntax-highlight, mermaid, and code-copy from touching
React-managed article content; emit flat doc HTML for canonical URLs; add SEO
sitemap/robots and trailing-slash normalisation so sidebar clicks stay stable.

Co-authored-by: Cursor <[email protected]>
2026-08-26 17:17:18 +01:00
MervinPraisonandCursor a391d55f74 fix(frontend): fetch content only when currentPath changes
Avoid re-running the markdown fetch when selectedItem syncs after
aiui:navigate, which was cancelling requests and leaving loading stuck.

Co-authored-by: Cursor <[email protected]>
2026-08-26 14:32:03 +01:00
MervinPraisonandCursor a5e6316021 fix(frontend): unstick "Loading content..." after SPA navigation
Reset loadingContent in effect cleanup when a fetch is cancelled, and
fetch only on currentPath changes so selectedItem sync does not abort
the in-flight request.

Co-authored-by: Cursor <[email protected]>
2026-08-26 14:31:48 +01:00
MervinPraisonandCursor 3c534c9637 fix(frontend): map /docs/index to /docs/index.md not /docs.md
Stripping a trailing /index segment broke the docs home page fetch,
causing "Content for PraisonAIUI not found" at /docs/index.

Co-authored-by: Cursor <[email protected]>
2026-08-26 14:30:49 +01:00
MervinPraisonandCursor e2486fe666 fix(frontend): stop empty pages and stale content on SPA nav
React now owns sidebar navigation content: sync selectedItem on
aiui:navigate, fetch markdown from currentPath with cancel-on-nav,
use canonical URLs without trailing slashes, and add data-nav-path on
sidebar buttons. Plugins teardown hide-mode instead of replacing React
DOM; mkdocs-compat no longer wipes main.flex-1.

Co-authored-by: Cursor <[email protected]>
2026-08-26 14:08:55 +01:00
MervinPraisonandCursor 60e9bbe2df fix(frontend): wrap theme tokens in hsl() so backgrounds render
Tailwind utilities used var(--background) directly, but shadcn tokens
store bare HSL components — backgrounds were transparent while prose
text used hsl(var(--foreground)), causing light-on-white in dark mode.
Also replace hardcoded dark anti-flicker with localStorage-aware bootstrap.

Co-authored-by: Cursor <[email protected]>
2026-08-26 12:07:06 +01:00
MervinPraisonandCursor 672070cd13 fix(frontend): restore light-mode contrast on plugin SPA path
Align content-loader and homepage plugins with React prose classes,
extend CSS contrast rules to all article.prose, fix server anti-flicker
hardcoded dark colours, wire theme toggle with localStorage, and add
guardrail tests for the plugin rendering path.

Co-authored-by: Cursor <[email protected]>
2026-08-26 11:23:46 +01:00
MervinPraisonandCursor db0672feec fix(frontend): restore light-mode prose and code contrast
Scope mermaid heading overrides to dark mode, fix hsl() theme tokens in
syntax-highlight, and force readable prose colours for body text, tables,
and code blocks in light mode.

Co-authored-by: Cursor <[email protected]>
2026-08-26 07:50:25 +01:00
MervinPraison 7fe03534de Release v0.3.142 2026-08-26 01:00:01 +00:00
MervinPraisonandCursor 23a193eb22 fix(frontend): complete docs UI P1 — routing, nav, TOC, mermaid
Wire route-manifest template resolution, mobile nav drawer, live TOC from
page headings, Mermaid async rendering via aiui:content-loaded, and
FlexibleLayout footer deduplication.

Co-authored-by: Cursor <[email protected]>
2026-08-25 23:58:43 +01:00
MervinPraison b6dc50180b Release v0.3.141 2026-08-25 22:48:37 +00:00
MervinPraisonandCursor d91c3baf21 fix(frontend): improve docs UI contrast and MkDocs rendering
Fix unreadable light-mode code blocks, transform MkDocs Material syntax
(tabs, icons, grid cards) in both React and plugin paths, and auto-select
nav items from the URL including top-level entries like /docs/index.

Co-authored-by: Cursor <[email protected]>
2026-08-25 23:45:46 +01:00
MervinPraisonandCursor 5c9e8b2356 docs(adp): harden protocol spec after multi-agent validation
Split component catalog schema from instance data, fix theme/slots
validation, clarify navigation model, add implementation status and
CI tests for ADP JSON Schema conformance.

Co-authored-by: Cursor <[email protected]>
2026-08-25 22:47:42 +01:00
MervinPraison a48f3ae4b2 fix(ci): enhance merge gate recovery and streamline auto-merge process 2026-08-25 22:40:35 +01:00
MervinPraison b4d705ce12 fix(ci): recognise Claude replies and unblock stale-FINAL after rebase
Port merge-gate recovery fixes from PraisonAI-Tools so auto-merge resumes after
conflict rebase pushes and MervinPraison Claude finished comments count.
2026-08-15 13:33:36 +01:00
MervinPraison 88c84d0c35 Release v0.3.140 2026-08-01 02:22:54 +00:00
MervinPraison 6d71fa4197 fix: port full scope-creep gates from PraisonAI with UI wording
Align merge gate, conflict/CI fix prompts, FINAL phases, and claude.yml
with PraisonAI lightweight-and-powerful checks — product value, layering
(praisonaiagents / PraisonAI-Tools), and server.py bloat — not Agent-class.
2026-07-31 14:04:16 +01:00
praisonai-triage-agent[bot] 576164960f Merge pull request #250 from MervinPraison/claude/issue-240-20260727-0912
feat: surface degraded knowledge search when embedding model unavailable
2026-07-31 12:08:20 +00:00
praisonai-triage-agent[bot] 9ec612b362 Merge pull request #249 from MervinPraison/claude/issue-235-20260727-0910
fix: prevent cp1252 UnicodeEncodeError in dashboard examples
2026-07-31 12:04:29 +00:00
praisonai-triage-agent[bot] 56760f843c Merge pull request #248 from MervinPraison/claude/issue-238-20260727-0912
fix: require explicit opt-in for GATEWAY_AUTH_TOKEN url auth
2026-07-31 12:00:49 +00:00
praisonai-triage-agent[bot] 39123500d5 Merge pull request #214 from MervinPraison/claude/issue-209-20260714-fix
fix: dedupe aiui:prefill-composer listener across chat re-navigations
2026-07-31 11:57:08 +00:00
praisonai-triage-agent[bot] 4098e5da4b Merge pull request #213 from MervinPraison/claude/issue-207-20260714-fix
fix: overview approval polling stacks duplicate click handlers (#207)
2026-07-31 11:53:16 +00:00
MervinPraison b3daa2fcfa Release v0.3.139 2026-07-31 11:52:38 +00:00
praisonai-triage-agent[bot] 7c7c121940 Merge pull request #215 from MervinPraison/claude/issue-211-20260714-work-hub-chat
feat: Work Hub Chat tab sends messages to agent via existing transport
2026-07-31 11:49:54 +00:00
praisonai-triage-agent[bot] c1da3fe0ae Merge pull request #212 from MervinPraison/claude/issue-210-20260714-1010
fix: honest gateway reachability probe on onboarding Screen 1
2026-07-31 11:48:48 +00:00
praisonai-triage-agent[bot]andMervin Praison a44776b18e fix: surface AC-4 opt-in on cross-origin CORS block in gateway probe
The reachability probe used default cors mode, so a cross-origin CORS
block rejected the fetch (landing in the 'network' branch) instead of
producing an opaque response. That made the 'opaque' branch and its
"Continue anyway" checkbox (AC-4) dead code. Now a rejected fetch on a
cross-origin URL maps to 'opaque' (ambiguous: CORS-blocked or down) so
the opt-in checkbox renders, while same-origin rejections remain a
definitive 'network' error.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-31 10:56:40 +00:00
MervinPraison 0e095d73fe fix: unblock FINAL @claude when review bots stay silent
Port PraisonAI review-chain parity: 30-minute required-reviewer timeout,
skip Copilot in scheduled FINAL sweep, and selftests for timeout path.
2026-07-31 11:53:09 +01:00
praisonai-triage-agent[bot]andMervin Praison d9836a955e fix: drop superseded #238 token-auth changes (already landed via #251)
Main's #251/#253/#258 implements the #238 fix with a stricter, more secure
design: AIUI_URL_TOKEN / GATEWAY_AUTH_TOKEN alone are inert; URL-token auth
activates only on explicit opt-in (AIUI_REQUIRE_TOKEN, YAML auth.requireToken,
require_auth). This branch's earlier variant treated AIUI_URL_TOKEN presence
(and YAML auth.urlToken) as an opt-in signal, which forced 401 and broke
test_token_auth_optin.py::test_aiui_url_token_alone_does_not_force_401.

Revert server.py to main's canonical logic and drop the now-conflicting
tests/unit/test_gateway_token_optin.py (its cases are covered by main's
tests/unit/test_token_auth_optin.py).

Co-authored-by: Mervin Praison <[email protected]>
2026-07-30 06:34:03 +00:00
praisonai-triage-agent[bot]andMervin Praison 0f986461f9 fix: require explicit opt-in for GATEWAY_AUTH_TOKEN url auth (fixes #238)
GATEWAY_AUTH_TOKEN leaked into an interactive shell no longer silently
enforces 401 on every /api/* route in standalone create_app(). Enforcement
now requires explicit intent via AIUI_URL_TOKEN, AIUI_REQUIRE_TOKEN=1, or
YAML auth.urlToken. Secured gateway deployments keep working.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-30 06:32:39 +00:00
MervinPraisonandClaude 6ffc84e9ba fix: scope overview approval re-bind to re-rendered subtrees (fixes #207)
refreshApprovals re-rendered only #ov-metrics and #ov-attention but then
called bindEvents on the whole overview root, stacking click/keydown
listeners on unchanged shell nodes (agent rows, omnibar) on every 10s poll.
Scope the re-bind to the two re-rendered subtrees so stale nodes keep a
single listener. paint() still does the full-tree bind on initial render.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-30 06:32:21 +00:00
MervinPraisonandClaude d4aa8c2ec4 feat: wire Work Hub Chat tab to agent via shared mini-chat transport (fixes #211)
Add mini-chat.js reusing the existing /api/chat/ws protocol (run_content/
run_completed/run_error) and wire work-hub.js renderChat() to stream real
agent replies instead of only appending a local DOM bubble. Sessions/agents
rebind per card and the WebSocket is torn down on card/tab switch.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-30 06:32:14 +00:00
MervinPraisonandClaude c4be709f8d fix: dedupe aiui:prefill-composer listener across chat re-navigations (fixes #209)
Register the Code Studio prefill listener as a module-scope singleton via
ensurePrefillListener() and remove it in cleanup(), preventing duplicate
window handlers from stacking on repeated SPA navigation between /chat and
/code-studio.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-30 06:32:06 +00:00
praisonai-triage-agent[bot]andMervin Praison 92adfc6f6e fix: honest gateway reachability probe on onboarding Screen 1 (fixes #210)
Co-authored-by: Mervin Praison <[email protected]>
2026-07-30 06:32:04 +00:00
MervinPraison 90315326f7 Release v0.3.138 2026-07-30 01:56:27 +00:00
MervinPraison 0719c41399 fix: auto-trigger conflict and CI fixes without FINAL review gate
Remove the FINAL @claude prerequisite from merge-conflict and CI-failure
workflows, clear pipeline/blocked:conflict after rebase completion, add
bot-pr-recovery cron, and update gate selftests.
2026-07-29 18:15:42 +01:00
praisonai-triage-agent[bot] d008b6a250 Merge pull request #216 from MervinPraison/claude/issue-208-20260714-i18n-memory
feat: wire i18n strings and per-turn memory search into context ring UI
2026-07-29 14:38:03 +00:00
praisonai-triage-agent[bot]andMervin Praison 05fb59e890 feat: surface degraded knowledge search when embedding model unavailable (fixes #240)
Rebased onto latest main. Reconciled with the overlapping degraded-mode
work already merged from #234:
- Adopt main's canonical _is_embedding_error / _embedding_error naming,
  embedding health dict, and richer /v1/embeddings fallback (503 vs 500).
- Keep _is_embedding_unavailable helper (used by main's test suite).
- Align SimpleKnowledgeManager fallback-test to main's "local" mode.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>
2026-07-29 13:11:17 +00:00
praisonai-triage-agent[bot]andMervin Praison 8067429e83 fix: prevent cp1252 UnicodeEncodeError in dashboard examples (fixes #235)
Add examples/python/_shared/console.py with safe_print/icon helpers
mirroring the CLI _supports_unicode/_icon pattern, and wire safe_print
into the gateway and channel examples so emoji output degrades to ASCII
on legacy Windows consoles instead of crashing before the server starts.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-29 13:07:27 +00:00
MervinPraisonandClaude 631ae74a29 feat: wire i18n strings and per-turn memory search into context ring UI (fixes #208)
Gap A: chat.js now loads locale strings via /api/i18n and renders ring
tooltip, compaction banner, compacted flash, and memory chip through a
t() helper with English fallback.

Gap B: refreshMemoryChip() posts the last user message to
/api/memory/search and shows per-turn hit count instead of the total
stored memory count from GET /api/memory.

Gap C: role="progressbar" and aria-value* moved to the ring wrapper div;
the send button is now type="button" with a send-only label.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-29 13:05:17 +00:00
praisonai-triage-agent[bot] e85f6778dd Merge pull request #258 from MervinPraison/claude/issue-256-20260729-120000
fix: stop full-suite pollution flaking auth timing + health cache tests
2026-07-29 11:39:19 +00:00
praisonai-triage-agent[bot] d6354fade0 Merge pull request #257 from MervinPraison/claude/issue-255-20260729-000000
fix: Windows-safe test_default_sqlite_url (stub HOME/USERPROFILE)
2026-07-29 11:35:43 +00:00
MervinPraisonandClaude b16a5f6056 fix: stop full-suite pollution flaking auth timing + health cache tests (fixes #256)
Reset the global _features registry in the test_server client fixture so a
feature leaked by an earlier crashing test cannot slow /health and flake the
deep-health cache/timing assertions. Make test_timing_attack_resistance
noise-resistant by comparing the median of repeated samples with a warm-up
instead of a single sample, so CPU contention under the full suite no longer
trips the variance threshold.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-29 09:28:32 +00:00
MervinPraisonandClaude 6521beb7b2 fix: stub HOME/USERPROFILE in test_default_sqlite_url for Windows (fixes #255)
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-29 09:21:35 +00:00
MervinPraison 9e3384c40a Release v0.3.137 2026-07-29 02:06:56 +00:00
praisonai-triage-agent[bot] e12416e15b Merge pull request #254 from MervinPraison/claude/issue-252-20260728-000000
fix: make five unit tests pass on Windows
2026-07-28 11:38:05 +00:00
MervinPraison 5568b10077 Release v0.3.136 2026-07-28 11:33:51 +00:00
praisonai-triage-agent[bot] 557852b287 Merge pull request #253 from MervinPraison/claude/issue-251-20260728-000000
fix: make URL-token auth opt-in so env token no longer forces 401
2026-07-28 11:30:52 +00:00
MervinPraisonandClaude 652d612d35 fix: make five unit tests pass on Windows (fixes #252)
Resolve cross-platform failures caused by Windows' coarse time.time()
resolution, backslash path separators, and reliance on the grep binary.

- lifecycle: measure hook durations with time.perf_counter() so
  startup_time/shutdown_time are non-zero on Windows' low-res wall clock
- datastore: build the default sqlite URL with Path.as_posix() for a
  consistent forward-slash URL across platforms
- test_audio_hooks: allow last_chunk_at == started_at (clock granularity)
- test_sqlalchemy_datastore: compare against as_posix() paths
- test_unified_naming: replace grep subprocess with a portable Python scan

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-28 10:17:52 +00:00
MervinPraisonandClaude 1f578a4827 fix: make URL-token auth opt-in so env token no longer forces 401 (fixes #251)
TokenQueryMiddleware previously activated whenever GATEWAY_AUTH_TOKEN or
AIUI_URL_TOKEN was present in the process environment, forcing 401 on all
standalone /api/* routes. A token leaking into a developer shell after a
gateway run broke ad-hoc TestClient scripts (#238, #251).

Activation now requires explicit opt-in: AIUI_REQUIRE_TOKEN, YAML
auth.requireToken, or require_auth=True. Secured deployments keep full
Bearer / query-param / cookie token auth.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-28 10:16:03 +00:00
praisonai-triage-agent[bot] 84ebdbc695 Merge pull request #247 from MervinPraison/claude/issue-236-20260727-0911
fix: add UTF-8 encoding to frontend source test reads
2026-07-27 12:23:34 +00:00
praisonai-triage-agent[bot] 182d065524 Merge pull request #246 from MervinPraison/claude/issue-239-20260727-120000
fix: correct tracing integration test paths to /api/traces
2026-07-27 12:18:36 +00:00
praisonai-triage-agent[bot] 132018d123 Merge pull request #245 from MervinPraison/claude/issue-237-20260727-0911
fix: use explicit UTF-8 encoding in CLI integration test fixture
2026-07-27 12:13:41 +00:00
MervinPraison bb85860a33 Release v0.3.135 2026-07-27 12:11:31 +00:00
praisonai-triage-agent[bot] c074d0782c Merge pull request #244 from MervinPraison/claude/issue-234-20260727-fix
feat: configurable embedding model + degraded knowledge search reporting
2026-07-27 12:08:27 +00:00
praisonai-triage-agent[bot] f7e19b9a13 Merge pull request #243 from MervinPraison/claude/issue-233-20260727-0904
fix: correct tracing API test paths (/api/tracing -> /api/traces)
2026-07-27 12:02:58 +00:00
praisonai-triage-agent[bot] 1a91f574b9 Merge pull request #242 from MervinPraison/claude/issue-231-20260727-0857
feat: safe_print for examples to avoid cp1252 UnicodeEncodeError
2026-07-27 11:58:32 +00:00
praisonai-triage-agent[bot] 0c982f4959 Merge pull request #241 from MervinPraison/claude/issue-232-20260727-0900
fix: specify UTF-8 encoding for frontend source reads in tests
2026-07-27 11:53:17 +00:00
MervinPraisonandClaude 0cc7f70bbb fix: correct tracing integration test paths to /api/traces (fixes #239)
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-27 09:14:24 +00:00
praisonai-triage-agent[bot]andpraisonai-triage-agent[bot] 87c9ed09b4 fix: add UTF-8 encoding to frontend source test reads (fixes #236)
Standardize Path.read_text() calls across unit/integration tests to
explicitly use encoding="utf-8". On Windows the default locale is cp1252,
which raises UnicodeDecodeError when reading committed UTF-8 frontend
bundles (em dash, emoji, multi-byte chars), producing false negatives.

Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
2026-07-27 09:14:23 +00:00
praisonai-triage-agent[bot]andMervin Praison 30c34e7266 fix: use explicit UTF-8 encoding in CLI integration test fixture (fixes #237)
Test fixture wrote YAML/markdown and read JSON without encoding, defaulting
to cp1252 on Windows and breaking UTF-8 reads for the © character. Specify
encoding="utf-8" on all write_text/read_text calls for deterministic
cross-platform behaviour.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-27 09:13:28 +00:00
MervinPraisonandClaude 43b9fe7253 feat: configurable embedding model + degraded-mode reporting for knowledge search (fixes #234)
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-27 09:10:15 +00:00
praisonai-triage-agent[bot]andMervin Praison 5b61ece40d fix: correct tracing API test paths /api/tracing to /api/traces (fixes #233)
Co-authored-by: Mervin Praison <[email protected]>
2026-07-27 09:05:29 +00:00
praisonai-triage-agent[bot]andMervin Praison 6fd27d0ed7 feat: safe_print for examples to avoid cp1252 UnicodeEncodeError (fixes #231)
Add examples/python/_shared/console.py with safe_print/icon helpers that
degrade emoji to ASCII on legacy Windows code pages (cp1252) instead of
crashing, mirroring the CLI _supports_unicode/_icon pattern. Wire the helper
into the high-severity gateway (16, 17, 18) and channel (20-23) examples so
they start on default Windows PowerShell without PYTHONUTF8.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-27 09:04:48 +00:00
praisonai-triage-agent[bot]andMervin Praison 78a8a499c1 fix: specify UTF-8 encoding for frontend source reads in tests (fixes #232)
Standardize all Path.read_text() calls in the test suite to pass
encoding="utf-8" so committed UTF-8 frontend JS/HTML bundles decode
correctly on Windows (cp1252 default locale) instead of raising
UnicodeDecodeError.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-27 09:03:59 +00:00
praisonai-triage-agent[bot]andMervin Praison 4d685af1da fix: correct timeline row selected-highlight CSS selector in /runs
The selected-row highlight targeted .rr-timeline-row.selected but rows
render with class .db-timeline-row, so clicking a timeline row never
visually highlighted it. Align the selector to .db-timeline-row.selected.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>
2026-07-16 07:55:06 +00:00
MervinPraisonandClaude a1c7a3c279 feat: add Run Flight Recorder /runs three-pane debugger (fixes #217)
Merges chat turns, trace spans, log lines and errors into one per-session
timeline. Reuses existing APIs only. Adds formatDuration/emptyState/
traceStatusBadge/timelineRow helpers, registers the runs page (Control,
order 22), and deep-links from Traces, Command Center and Work Hub.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-16 07:55:06 +00:00
MervinPraison 01a31f7c8b Release v0.3.134 2026-07-16 02:07:47 +00:00
praisonai-triage-agent[bot] fc5ce8a30c Merge pull request #230 from MervinPraison/claude/issue-223-20260715-finops
feat: Cost FinOps Strip on Overview and Chat (STITCH-013)
2026-07-15 16:45:49 +00:00
MervinPraisonandClaude c9985de128 feat: Cost FinOps Strip on Overview and Chat (fixes #223)
Add STITCH-013 ambient spend awareness reusing existing usage, config
and context-stats APIs. New _helpers.js FinOps utilities (MiniDonutSVG,
budget math, CostStrip/BudgetBanner markup); Overview CostStrip +
BudgetBanner with graceful degradation; Chat SessionCostChip + budget
banner updating on run_completed; FinOps config schema section with
cross-field validation; finops.* i18n keys (en/es/fr); unit tests.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-15 15:25:36 +00:00
praisonai-triage-agent[bot] c3e330454c Merge pull request #227 from MervinPraison/claude/issue-218-20260715-000000
feat: Skills Studio — pending/diff/history for self_improve skill writes
2026-07-15 15:23:05 +00:00
praisonai-triage-agent[bot] 203b885d6b Merge pull request #226 from MervinPraison/claude/issue-220-20260715-1317
feat: Channel Inbox — unified multi-channel message hub (STITCH-010)
2026-07-15 15:19:10 +00:00
praisonai-triage-agent[bot] 9a543164aa Merge pull request #225 from MervinPraison/claude/issue-219-20260715-1317
feat: Eval Quality Cockpit — regression dashboard (STITCH-009)
2026-07-15 15:13:56 +00:00
praisonai-triage-agent[bot] 8eb9d0a75c Merge pull request #224 from MervinPraison/claude/issue-221-20260715-000000
feat: session fork endpoint for Agent Playground (STITCH-011)
2026-07-15 15:08:36 +00:00
praisonai-triage-agent[bot]andMervin Praison 10f3bbe1ce fix(training): correct agent_id filter, apply dropdown source, and view nav order
- features/training.py: derive agent_id in _summarize_session so the
  /api/training/sessions?agent_id= filter actually matches (was always None).
- training-lab.js: fetch /api/agents/definitions (returns `id`) instead of
  /api/agents (returns `agent_id`) so the apply dropdown carries valid agent
  IDs matching the registry; fall back to agent_id defensively.
- agents.js: set the agent_id URL param before selectPage('training') so the
  Training Lab picks up the filter on initial render.
- tests: add regression test for agent_id session filtering.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-15 13:47:40 +00:00
praisonai-triage-agent[bot]andMervin Praison 1170b264ee fix: preserve session in eval debug deep-link before navigating
The debug-run deep link only pushed the /runs?session= URL state in the
traces fallback branch, dropping the session when a runs page exists.
Push the URL state before navigating in both cases so Run Flight Recorder
(STITCH-007) receives session context when it lands.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-15 13:43:55 +00:00
praisonai-triage-agent[bot]andMervin Praison f0a6b16626 fix: persist fork lineage metadata on default datastores
The session fork endpoint recorded parent_session_id/agent_id/source via
ds.update_session(metadata=...), but the shipped default stores dropped it:
SDKFileDataStore.update_session only handled title/platform/icon and
JSONFileDataStore had no override at all (base no-op). Fork lineage only
survived on the volatile MemoryDataStore used in tests, silently failing in
production.

- SDKFileDataStore: persist custom session metadata in the sentinel meta
  message and surface it via get_session().
- JSONFileDataStore: add update_session override persisting arbitrary kwargs
  (also fixes title updates being lost).
- Add regression test asserting fork metadata persists on the default stores.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>
2026-07-15 13:43:50 +00:00
praisonai-triage-agent[bot]andMervin Praison c66a33c5d3 feat: add Training Lab UI for praisonai-train sessions (fixes #222)
Co-authored-by: Mervin Praison <[email protected]>
2026-07-15 13:26:32 +00:00
MervinPraisonandClaude 6608e17303 feat: add Skills Studio pending/diff/history workflow (fixes #218)
Refactor skills.js into a tabbed Skills Studio (Pending, Installed,
History) that surfaces self_improve skill-write approvals, adds a
side-by-side line diff with raw/preview toggle, and wires approve/deny
to the existing /api/approvals endpoints. Adds diffLines,
renderMarkdownPreview and isSkillApproval helpers, a cross-link from
approvals.js for skill rows, and skills.* i18n keys.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-15 13:24:41 +00:00
praisonai-triage-agent[bot]andMervin Praison 91ff46dcb1 feat: add Channel Inbox unified multi-channel message hub (fixes #220)
Track B session-filter implementation: new /inbox three-pane view that
derives threads from channel-backed sessions + chat history, persists
status/assignee via session state, replies through the chat transport,
and deep-links to /chat and /work. Surfaces platform/channel_id in the
sessions list API.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-15 13:24:25 +00:00
praisonai-triage-agent[bot]andMervin Praison 732bd123d8 feat: Eval Quality Cockpit regression dashboard (fixes #219)
Refactor the read-only /eval table into a quality cockpit: status strip,
hero metrics (pass rate, avg score, regressions, total cases), per-agent
suite cards with 7-day sparklines and trend arrows, localStorage baseline
compare, in-UI run trigger (POST /api/eval/run), and expandable history
rows that deep-link failed cases to Run Flight Recorder (/runs?session=)
with a /traces fallback. All trend/regression logic is client-derived from
existing endpoints; no backend changes.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-15 13:23:57 +00:00
MervinPraisonandClaude 71931db0c6 feat: add session fork endpoint for Agent Playground (fixes #221)
POST /api/sessions/{id}/fork branches a chat session into a new child,
copying messages up to an optional up_to_index and recording parent
metadata. Backend primitive for the Agent Playground (STITCH-011).

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-15 13:22:59 +00:00
MervinPraison ffa3060f0b Release v0.3.133 2026-07-15 01:57:26 +00:00
praisonai-triage-agent[bot] 6d194fce1b Merge pull request #205 from MervinPraison/claude/issue-200-20260714-code-studio
feat: Code Studio — Monaco editor with sandbox execute and Send to Agent (STITCH-006)
2026-07-14 09:32:47 +00:00
praisonai-triage-agent[bot] 4ecb6497b5 Merge pull request #206 from MervinPraison/claude/issue-196-20260714-context-ring
feat: composer context token ring, memory chip & compaction banner
2026-07-14 08:53:57 +00:00
praisonai-triage-agent[bot] fbd1310639 Merge pull request #203 from MervinPraison/claude/issue-197-20260714-0719
feat: Unified Work Hub /work (STITCH-003)
2026-07-14 08:49:56 +00:00
praisonai-triage-agent[bot] 198c67528c Merge pull request #202 from MervinPraison/claude/issue-198-20260714-0719
feat: first-run onboarding gate for gateway auth (STITCH-004)
2026-07-14 08:46:48 +00:00
praisonai-triage-agent[bot] 303e6305e6 Merge pull request #201 from MervinPraison/claude/issue-195-20260714-0719
feat: Agent Command Center overview (STITCH-001)
2026-07-14 08:42:58 +00:00
MervinPraisonandClaude 17689c4468 feat: add Code Studio view with sandbox execute and Send to Agent (fixes #200)
Add /code-studio dashboard view: lazy Monaco editor, language picker,
sandbox execution via existing /api/code/* endpoints, output/errors/artifacts
tabs, client-side run history, snippet save, and Send to Agent handoff that
prefills the chat composer via aiui:prefill-composer.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-14 08:41:40 +00:00
praisonai-triage-agent[bot] f0d08f1cc2 Merge pull request #204 from MervinPraison/claude/issue-199-20260714-000000
feat: plugin marketplace dashboard page (STITCH-005)
2026-07-14 08:39:29 +00:00
praisonai-triage-agent[bot]andMervin Praison 32d27c20c8 fix: escape server-provided plugin fields in marketplace view (XSS)
Interpolated plugin name/description/id/category/version/permissions into
innerHTML without escaping, unlike sibling views (mcp.js, channels.js,
chat.js) which use an esc() helper. Add the same esc() helper and apply it
for defense-in-depth and convention consistency.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-14 07:46:42 +00:00
praisonai-triage-agent[bot]andMervin Praison c52728c9e3 fix: hoist pendingHandled/tryDeepLink above board init in work-hub
Declaring pendingHandled (let) and tryDeepLink after the
createInteractiveBoard call that closes over them was a temporal-dead-zone
hazard. Move them above the board construction and simplify the onOpen
deep-link guard.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-14 07:44:56 +00:00
praisonai-triage-agent[bot]andMervin Praison da9d7425b9 fix: apply onboarding gateway token to all dashboard API calls
The first-run onboarding gate stored the gateway bearer token in
sessionStorage but no dashboard data fetch (/api/pages, /ui-config.json,
view modules, /api/health, …) attached it, so protected APIs kept
returning 401 after "Connect" — defeating the feature. Install a
one-time window.fetch wrapper in init() that injects the stored bearer
token on same-origin requests when present, preserving any explicit
Authorization header and remaining a no-op in standalone/no-token mode.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-14 07:44:50 +00:00
praisonai-triage-agent[bot]andMervin Praison 0b0dfb6f5f fix: wire 'Chat now' deep-link to existing ?agent= param
The overview dispatched an aiui:select-agent CustomEvent that had no
listener anywhere, so 'Chat now'/'Chat' buttons navigated to the chat
page but never pre-selected the agent (dead deep-link). Replace it with
the established ?agent=<name> URL param that chat.js already reads on
mount (chat.js:771), matching the canonical selectSession pattern.
Pass agent name (option value) instead of id so the selector matches.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-14 07:43:15 +00:00
MervinPraisonandClaude b84b9d3ee7 feat: composer context token ring, memory chip, compaction banner (fixes #196)
Add a server-driven context budget indicator to the chat composer:
- GET /api/chat/context-stats returns a char-based token estimate with a
  deterministic breakdown (system/tools/messages/memory).
- Token ring wraps the send button with healthy/warning/critical/over
  states, dashed arc for estimates, and a breakdown tooltip.
- Compaction banner surfaces at >=85% usage; a memory chip shows retrieved
  memory count per turn and opens the existing memory panel.
- Ring refreshes on run completion, WS context_update/context_compacted
  events, and a 5s poll fallback.
- i18n keys added for en/es/fr.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-14 07:27:36 +00:00
MervinPraisonandClaude 74260cd68a feat: add plugin marketplace dashboard page (fixes #199)
Add a /marketplace dashboard view that browses, inspects permissions,
installs, and uninstalls plugins via the existing features/marketplace.py
backend. Registers the view in dashboard.js BUILTIN_VIEWS and adds the
page entry (Agent group) with the /api/marketplace/plugins endpoint.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-14 07:26:24 +00:00
praisonai-triage-agent[bot]andMervin Praison 00bbdd29a6 feat: add Unified Work Hub /work three-pane surface (fixes #197)
Co-authored-by: Mervin Praison <[email protected]>
2026-07-14 07:25:23 +00:00
praisonai-triage-agent[bot]andMervin Praison b3c62a9cc0 feat: first-run onboarding gate for gateway auth (fixes #198)
Add a guided 4-step onboarding overlay shown on first visit when the
dashboard runs in gateway mode and the user is unauthenticated,
replacing the raw HTTP 401 experience. The gate runs in dashboard.js
init() before buildDashboard() and skips for standalone/authenticated
users and when the "don't show again" flag is set.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-14 07:25:00 +00:00
praisonai-triage-agent[bot]andMervin Praison 7998b8ccc0 feat: Agent Command Center overview with live agents, approvals, usage (fixes #195)
Replace the single-endpoint Overview with a mission-control home that fetches
9 APIs in parallel (Promise.allSettled): 6 live metric cards with sparklines,
an active-agents table with a detail drawer, a priority-sorted needs-attention
feed with inline approve/deny, a recent-activity timeline, and an omnibar CTA
that opens the existing Ctrl+K command palette. Polling pauses on hidden tabs
and intervals are torn down on cleanup. All API strings are HTML-escaped.

Adds reusable metricCard, sparklineSVG, esc and timeAgo helpers to _helpers.js.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-14 07:24:15 +00:00
MervinPraison 2ebb56f609 Release v0.3.132 2026-07-14 02:00:30 +00:00
praisonai-triage-agent[bot] 6725a5bdf7 Merge pull request #194 from MervinPraison/claude/issue-191-20260713-0819
fix: honor aiui run --port for gateway examples that delegate to async main()
2026-07-13 10:12:12 +00:00
MervinPraison f1f953be76 Release v0.3.131 2026-07-13 10:08:48 +00:00
praisonai-triage-agent[bot] ee994b586d Merge pull request #193 from MervinPraison/claude/issue-192-20260713-120000
fix: isolate OPENAI_API_KEY in realtime lifecycle test
2026-07-13 10:04:40 +00:00
praisonai-triage-agent[bot]andMervin Praison 44428e5caf fix: propagate --port/--host to gateway main() delegation (fixes #191)
Set PORT/HOST env before delegating to a user-defined async main() so
gateway examples that read os.getenv("PORT") honor the CLI --port flag.
Explicit env set by the user takes precedence over the CLI flag.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-13 08:25:16 +00:00
MervinPraisonandClaude 1748d6d023 fix: isolate OPENAI_API_KEY in realtime session lifecycle test (fixes #192)
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-13 08:21:25 +00:00
MervinPraison a45c3af006 Release v0.3.130 2026-07-10 10:49:34 +00:00
praisonai-triage-agent[bot] b54d94dd56 Merge pull request #190 from MervinPraison/claude/issue-189-20260710-0829
feat: restore Channels dashboard Add/Delete/Test CRUD UI
2026-07-10 10:46:59 +00:00
praisonai-triage-agent[bot]andMervin Praison d9e8b12628 feat: restore Channels dashboard Add/Delete/Test CRUD UI (fixes #189)
Restore the + Add Channel modal, per-platform credential fields, setup
hints, and card actions (Enable/Disable, Restart, Test, Delete) that were
removed in the 3961508 refactor. Aligned with #176 secret policy (env
references, no plaintext prefill, inline 400 errors surfaced) and #172
dropdown contrast fix (.db-form-select). Adds POST /api/channels preset to
the API Explorer and neutralizes the legacy read-only plugin so it no
longer clobbers the managed view.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-10 08:34:43 +00:00
MervinPraison 2fb299a454 Release v0.3.129 2026-07-09 17:05:28 +00:00
praisonai-triage-agent[bot] e288fca9ca Merge pull request #184 from MervinPraison/claude/issue-174-20260708-0849
fix: dashboard generator emits main before shellHeader insertBefore (#174)
2026-07-09 16:53:45 +00:00
praisonai-triage-agent[bot] f3dd9b218d Merge pull request #182 from MervinPraison/claude/issue-177-20260708-120000
feat: surface user-visible errors for offline chat send
2026-07-09 16:51:10 +00:00
praisonai-triage-agent[bot] 949a1862da Merge pull request #181 from MervinPraison/claude/issue-178-20260708-120000
fix: expose Ctrl+K command palette globally + guard init idempotency
2026-07-09 16:47:38 +00:00
praisonai-triage-agent[bot]andMervin Praison 7167546900 test: align shell-error assertions with showDashboardInitError after rebase
The rebase onto main kept main's showDashboardInitError() helper, which
supersedes this PR's showShellError() and renders the same .db-error
banner with role=alert. Update the RC-2 assertions to match the surviving
helper name; UX guarantee (visible shell error on missing #db-main-content)
is unchanged.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-09 16:10:20 +00:00
MervinPraisonandClaude e029f6e063 feat: surface user-visible errors for offline chat send (fixes #177)
Add defensive shell error UI in dashboard.js selectPage, offline-aware
HTTP fallback messaging with toast, role=alert on system error bubbles,
and a try/catch guard around ws.send in chat.js sendMessage.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-09 17:06:25 +01:00
praisonai-triage-agent[bot] 32e8552840 Merge pull request #173 from MervinPraison/claude/issue-172-20260630-120000
fix: dark-theme dropdown contrast and sidebar header overlap
2026-07-09 16:04:43 +00:00
Mervin Praison e31df90d0e Merge pull request #179 from MervinPraison/claude/issue-175-20260708-000000
fix: surface selectPage error when dashboard shell incomplete
2026-07-09 17:04:40 +01:00
praisonai-triage-agent[bot] 58c66b65e5 Merge pull request #180 from MervinPraison/claude/issue-176-20260708-120000
fix: redact channel secrets in all API responses
2026-07-09 16:04:10 +00:00
praisonai-triage-agent[bot]andMervin Praison f0c298802b fix: emit main before shellHeader insertBefore in dashboard generator (fixes #174)
The dashboard.js generator emitted root.insertBefore(shellHeader, main)
before root.appendChild(main), so regenerating the bundle re-introduced
the #166/#174 insertBefore NotFoundError. Reorder the generator to append
main first, and lock it with a source-parsing regression test.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-09 17:00:21 +01:00
praisonai-triage-agent[bot]andMervin Praison 45a5f0cd2d test: align session-search Ctrl+K test with command-palette ownership
The Ctrl+K/Cmd+K binding moved to initCommandPalette; initSessionSearch
now only owns Escape-to-close. Update test_ctrl_k_keydown_handler_present
to assert the current architecture instead of the outdated contract.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-09 17:00:18 +01:00
MervinPraisonandClaude 1b0e1646c9 fix: expose command palette globally and guard init idempotency (fixes #178)
BQA-002 (insertBefore) and the base command palette are already present in
the shipped dashboard bundle. This closes the remaining acceptance gaps:
openCommandPalette is now reachable in the global scope
(typeof openCommandPalette === 'function') and initCommandPalette no longer
double-registers its keydown handler on HMR / full reload.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-09 17:00:18 +01:00
praisonai-triage-agent[bot]andpraisonai-triage-agent[bot] 2fe1a83c9a fix(ci): guard find_spec so missing praisonai parent pkg skips instead of erroring
find_spec("praisonai.jobs") raises ModuleNotFoundError (not returns None)
when the parent 'praisonai' package is absent, aborting collection of the
entire tests/unit suite in CI. Wrap in a helper that treats the missing
parent as unavailable so the SDK test skips cleanly.

Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
2026-07-09 17:00:15 +01:00
praisonai-triage-agent[bot]andMervin Praison 971033b905 fix(channels): align write-path secret validation with read-path redaction
_validate_config_security used the narrow exact-match SECRET_FIELD_PATTERNS
while _redact_config_secrets was hardened to also catch *_token/*_key/*_secret/
*_password suffix keys. This closed the read leak but still let inline
app_token/signing_secret/client_secret values be stored on write. Route both
paths through the shared _is_secret_key helper so suffix secrets are rejected
at write time too, and add regression tests.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-09 17:00:15 +01:00
MervinPraisonandClaude 0626d0bb96 fix: redact channel secrets in all API responses (fixes #176)
GET /api/channels and related handlers serialized the in-memory _channels
dict verbatim, exposing config.bot_token and other secrets. Harden
_redact_config_secrets to cover *_token/*_key/*_secret/*_password suffix
keys (app_token, signing_secret, client_secret) and emit a <key>_set flag,
and redact the previously-unredacted _toggle response. Write path still
accepts and uses real tokens for bot startup.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-09 17:00:15 +01:00
MervinPraisonandClaude 581df433ed fix: surface selectPage error when dashboard shell incomplete (fixes #175)
Replace the silent `if (!main || !page) return;` early return in
selectPage() with explicit guards that log a console error and, when
#db-main-content is absent, render a visible db-error banner via the new
showDashboardInitError helper.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-09 17:00:13 +01:00
MervinPraisonandClaude bead6e4542 fix: dark-theme dropdown contrast and sidebar header overlap (fixes #172)
Native <select> option popups inherited the dashboard light foreground
while the browser painted a white system surface, producing unreadable
white-on-white platform labels. Add explicit dark-surface/light-text
styling for .db-form-select option and any dashboard select option, plus
an accent highlight for the checked option.

Harden the sidebar header so a long brand title truncates with an
ellipsis and reserves space for the toggle, preventing text overlap.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-09 17:00:11 +01:00
MervinPraison d0c8e060ff Release v0.3.128 2026-07-09 15:24:52 +00:00
Mervin Praison cdd6b21d3b Merge pull request #188 from MervinPraison/fix/merge-gate-stash-config
fix(merge-gate): stash gate-config.js with merge-gate.js
2026-07-09 16:21:34 +01:00
praisonai-triage-agent[bot]andMervin Praison b5f35214ef fix(ci): sync version and update stale registry/session-search tests
Pre-existing failures on main unrelated to the merge-gate workflow diff:
- sync __version__.py to 0.3.127 to match pyproject.toml
- i18n graduated to IMPLEMENTED (0.5.0); update feature-registry tests
- Ctrl+K binding is owned by initCommandPalette; assert the UX contract
  (Ctrl+K routes to session search) instead of the old implementation detail

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>
2026-07-09 15:03:46 +00:00
MervinPraison 2f73a94808 fix(merge-gate): stash gate-config.js with merge-gate.js 2026-07-09 15:57:21 +01:00
Mervin Praison 611c06f31b Merge pull request #187 from MervinPraison/fix/ci-sdk-job-store-import
fix(ci): skip SDK job store test when praisonai missing
2026-07-09 14:37:21 +01:00
MervinPraison 7f41bcbd9c fix(ci): skip SDK job store test when praisonai missing
find_spec(\"praisonai.jobs\") raises ModuleNotFoundError when the parent
package is absent, aborting pytest collection. Catch that and skip.
Also add claude-code-action and restore it after PR checkout for merge gate.
2026-07-09 14:30:26 +01:00
Mervin Praison f47b9f3824 Merge pull request #186 from MervinPraison/fix/ruff-channels-whitespace
fix(ci): clear ruff whitespace blocking main CI
2026-07-09 10:03:50 +01:00
MervinPraison 0e1a90b9f8 fix(ci): clear ruff W291/W293 whitespace in channels.py
Unblocks main and all open PRs blocked on python lint in CI.
2026-07-09 10:00:20 +01:00
Mervin Praison f86baee258 Merge pull request #185 from MervinPraison/infra/stale-final-recovery
infra: stale-FINAL recovery for open PRs
2026-07-08 10:56:33 +01:00
MervinPraison 69e469e71f infra: add stale-FINAL recovery for open PRs
Port claude-review-recovery and scheduled post-missing-claude-final from
PraisonAI, with allowStaleRepost so FINAL can be re-posted after new commits.
Remove duplicate bot-pr-trigger-reviews job.
2026-07-08 10:45:46 +01:00
Mervin Praison 1b2103e3a8 Merge pull request #183 from MervinPraison/infra/gate-setup
infra: merge gate, release gate, and pipeline automation
2026-07-08 09:48:11 +01:00
MervinPraison 88307cf5b0 infra: add merge gate, release gate, and pipeline automation
Port PraisonAI gate scripts with gate-config.js for PraisonAIUI paths,
single-package release preflight, pipeline label sync, Opus merge gate,
CI failure Claude trigger, pypi-auto patch releases, and review-chain hooks.
2026-07-08 09:36:56 +01:00
MervinPraison 140dd3a443 fix(version): update aiui to version 0.3.127 in project files
Updated the version in pyproject.toml and uv.lock to reflect the latest release.
2026-07-05 06:21:52 +01:00
MervinPraison 20b29caae3 chore(version): update aiui version to 0.3.126
Bumped the version in pyproject.toml and uv.lock to reflect the latest release.
2026-06-29 10:06:23 +01:00
MervinPraison d6cff26261 feat(version): update aiui version to 0.3.125
Incremented the version of the aiui project to 0.3.125 in both
pyproject.toml and uv.lock files to reflect the latest release.
2026-06-29 10:05:57 +01:00
Mervin Praison 7018271836 Merge pull request #171 from MervinPraison/claude/issue-170-20260628-120000
feat: world-class kanban board — dense cards, editable drawer, debounced live updates
2026-06-29 10:05:26 +01:00
Mervin Praison 61e5fd04ed Merge pull request #169 from MervinPraison/claude/issue-167-20260626-120000
fix: harden channel auto-start lifecycle and running state
2026-06-29 10:04:59 +01:00
Mervin Praison 80deeb451a Merge pull request #168 from MervinPraison/claude/issue-166-20260626-000000
fix: dashboard buildDashboard insertBefore NotFoundError (#166)
2026-06-29 10:04:46 +01:00
MervinPraisonandClaude 9726411010 feat: enrich kanban cards, editable drawer, debounced live updates (fixes #170)
Gap 1: card DTO now carries tenant, created_at, comment_count and child
progress; cardElement renders dense chrome (checkbox, status dot, id,
priority badge, tenant chip, assignee, age, comment count, progress pill).

Gap 2: new POST /api/kanban/tasks/{id}/comments route plus add_comment on
the store protocol; drawer is now editable (title/assignee/priority),
markdown-rendered (XSS-safe), shows a comment thread with Enter-to-submit
and run/event history rows.

Gap 3: SSE consumer refetches on a 300ms debounce, the open drawer
live-refreshes on events for its task, and destructive moves/delete prompt
for confirmation.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-06-28 04:23:35 +00:00
MervinPraisonandClaude 7cb20bca32 fix: harden channel auto-start lifecycle and running state (fixes #167)
Gateway _channel_tasks may be a dict or a list depending on SDK version;
calling .append() on a dict raised 'dict' object has no attribute 'append'
during auto-start. Register defensively for both shapes and never let a
gateway bookkeeping failure abort an already-started bot.

Also force running=False whenever start_error is set (in _sync_running_status
and the auto-start failure paths) so the API no longer reports running=true
alongside an error, and log full tracebacks via logger.exception.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-06-26 06:20:46 +00:00
MervinPraisonandClaude 11898f2c7d fix: append main before insertBefore in dashboard buildDashboard (fixes #166)
buildDashboard() called root.insertBefore(shellHeader, main) before main was
a child of root, throwing NotFoundError and aborting the dashboard plugin
init, which blocked Ctrl+K and the browser chat input. Append main to root
first so it is a valid reference node for the guarded header insert.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-06-26 06:19:37 +00:00
MervinPraisonandCursor 0b62e7dcec feat: add PyPI Release workflow for aiui package
Manual workflow_dispatch release matching PraisonAI: patch bump,
uv publish, PyPI wait, commit, tag, and GitHub release. Update
instruction.md with PraisonAIUI publish steps.

Co-authored-by: Cursor <[email protected]>
2026-06-25 07:48:12 +01:00
MervinPraisonandCursor fdef44b303 feat: add SDK/product value gate to Claude FINAL review prompts
Centralise PraisonAIUI scope and phase text in workflow env vars and
require Claude to evaluate genuine UI value, correct layering vs
praisonaiagents, and reject scope creep before approving PRs.

Co-authored-by: Cursor <[email protected]>
2026-06-25 07:47:13 +01:00
Mervin Praison 596bb2684c Merge pull request #164 from MervinPraison/claude/issue-162-20260625-0601
fix: sync __version__.py to 0.3.124 (fixes #162)
2026-06-25 07:46:27 +01:00
Mervin Praison 047b86b89f Merge pull request #165 from MervinPraison/claude/issue-163-20260625-0601
feat: parallelize legacy /health and add short-TTL cache
2026-06-25 07:46:10 +01:00
praisonai-triage-agent[bot]andpraisonai-triage-agent[bot] f60f81e086 feat: parallelize legacy /health and add short-TTL cache (fixes #163)
Run provider.health() concurrently with feature checks under a single
bounded gather instead of awaiting the provider first, and cache the deep
health payload for a short TTL so repeat probes stay fast.

Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
2026-06-25 06:04:59 +00:00
praisonai-triage-agent[bot]andMervin Praison 62d20465b5 fix: sync __version__.py to 0.3.124 (fixes #162)
Co-authored-by: Mervin Praison <[email protected]>
2026-06-25 06:02:41 +00:00
MervinPraison c2f8af6ad0 chore(release): bump version to 0.3.124
Updated version in pyproject.toml and uv.lock to reflect new release.
2026-06-24 23:50:34 +01:00
Mervin Praison 9bb312c8c4 Merge pull request #160 from MervinPraison/claude/issue-155-20260624-120000
feat: Ctrl+K session search in dashboard style (C-01, #155)
2026-06-24 23:50:10 +01:00
MervinPraison d07e41e69b fix(version): update aiui version to 0.3.123
Updated the version in pyproject.toml and uv.lock to reflect the new release.
2026-06-24 23:49:50 +01:00
MervinPraisonandClaude 043aa5c711 feat: port Ctrl+K session search to dashboard style (fixes #155)
Completes C-01 parity for the dashboard presentation plane. PR #145 shipped
SessionSearch in React layouts only, so the primary ops example
(15-dashboard-test, style="dashboard") never mounted React and Ctrl+K did
nothing. This adds a vanilla-JS command palette to dashboard.js (Ctrl+K /
Cmd+K toggle, debounced filter, arrow-key nav, Enter/click select, Esc close)
backed by GET /sessions, and wires chat.js to load the chosen session via an
aiui:session-select event.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-06-24 22:23:18 +00:00
MervinPraisonandCursor 4c5dadd255 fix: dedupe merge-conflict Claude triggers on main push bursts
Main pushes bypassed the 12h cooldown and concurrent merge runs each
posted @claude for every open DIRTY PR. Apply cooldown always, skip when
claude-conflict-pending label exists, cancel stale scans, and sync fork
handling plus FINAL review gate from PraisonAI.

Co-authored-by: Cursor <[email protected]>
2026-06-24 23:22:54 +01:00
Mervin Praison 86a6018c81 Merge pull request #156 from MervinPraison/claude/issue-153-20260624-0952
fix: sync __version__.py to 0.3.122 (fixes #153)
2026-06-24 23:18:29 +01:00
Mervin Praison 5f37843e3d Merge pull request #157 from MervinPraison/claude/issue-150-20260624-000000
fix(compiler): write all build output as UTF-8 (Windows cp1252)
2026-06-24 23:18:20 +01:00
Mervin Praison 4e2e14699c Merge pull request #158 from MervinPraison/claude/issue-151-20260624-000000
feat: implement /api/provider/health JSON route + guard /api/* catch-all
2026-06-24 23:18:06 +01:00
Mervin Praison 5e512d28da Merge pull request #159 from MervinPraison/claude/issue-152-20260624-0952
feat: parallelize legacy /health and migrate consumers to fast probes
2026-06-24 23:17:44 +01:00
Mervin Praison 420f3776d8 Merge pull request #161 from MervinPraison/claude/issue-154-20260624-0952
feat: MCP manager view + Ctrl+K command palette (B-08, B-02)
2026-06-24 23:17:08 +01:00
praisonai-triage-agent[bot]andMervin Praison 6afb282d76 feat: add MCP manager view and Ctrl+K command palette to dashboard (fixes #154)
Implements B-08 (MCP manager) and B-02 (command palette) from the OpenClaw
Control UI parity umbrella. MCP view lists/connects/disconnects servers via
the existing /api/mcp/* routes; command palette navigates pages via Ctrl+K.

Co-authored-by: Mervin Praison <[email protected]>
2026-06-24 09:58:56 +00:00
praisonai-triage-agent[bot]andMervin Praison 67137d4ba4 feat: parallelize legacy /health and migrate consumers to fast probes (fixes #152)
Legacy /health ran sequential per-feature health checks (~2.8s with 42
features). Parallelize with asyncio.gather + 0.5s per-feature timeout
matching /health/ready, and short-circuit ?deep=false to liveness.
Migrate CLI health-check/doctor status to /health/live and provider
status to /health/ready?deep=false. Update K8s/Docker probe docs to
/health/live.

Co-authored-by: Mervin Praison <[email protected]>
2026-06-24 09:57:46 +00:00
MervinPraisonandClaude faec8047b4 feat: implement /api/provider/health JSON route and guard /api/* catch-all (fixes #151)
Register a dedicated GET /api/provider/health handler before the SPA
catch-all so API clients receive JSON instead of dashboard HTML. Unmatched
/api/* paths now return a JSON 404 in both the static-mount fallback and the
path:path catch-all. Docs updated to match the implementation schema.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-06-24 09:57:43 +00:00
MervinPraisonandClaude 0d3baebaf3 fix(compiler): write all text output as UTF-8 (fixes #150)
Path.write_text/open default to the locale encoding (cp1252 on
Windows), causing UnicodeEncodeError when compiled HTML/JSON/CSS
contains non-ASCII content such as the U+2728 sparkle from docs.
Add explicit encoding="utf-8" to every text write in the build
and config pipeline.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-06-24 09:57:42 +00:00
praisonai-triage-agent[bot]andMervin Praison fe1bcd5e28 fix: sync __version__.py to 0.3.122 (fixes #153)
Co-authored-by: Mervin Praison <[email protected]>
2026-06-24 09:54:25 +00:00
MervinPraison ee62c7b908 feat(release): bump version to 0.3.122 and update dependencies
Updated the project version in `pyproject.toml` and `uv.lock`.
Changed dependency versions for `praisonai` and `praisonaiagents`
to their latest compatible versions, ensuring better stability and features.
2026-06-24 09:48:15 +01:00
Mervin Praison da9ff14884 Merge pull request #145 from MervinPraison/claude/issue-128-20260617-0955
feat: add session search palette with Ctrl+K shortcut
2026-06-24 09:47:14 +01:00
MervinPraison c55303684a chore: remove merge conflict CI test marker 2026-06-24 09:04:52 +01:00
MervinPraison d516d9e538 fix(ci): use GH_TOKEN PAT so @claude comment triggers claude.yml 2026-06-24 09:02:27 +01:00
MervinPraison a8d7e4598c fix(ci): grant pull-requests write for merge-conflict Claude comments 2026-06-24 09:00:40 +01:00
MervinPraison dcef9ff502 test: main-side merge conflict marker for CI automation test 2026-06-24 08:59:59 +01:00
MervinPraison fd038d054e ci: auto-trigger Claude on merge conflicts across PRs
Add merge-conflict-claude workflow and conflict rebase prompt with GraphQL merge_state detection in claude.yml.
2026-06-24 08:58:36 +01:00
Mervin Praison a2b941dd24 Merge pull request #146 from MervinPraison/claude/issue-132-20260623-0941
fix: Windows CLI UnicodeEncodeError for Rich output (fixes #132)
2026-06-23 12:33:09 +01:00
Mervin Praison 6d9d0727f2 Merge pull request #147 from MervinPraison/claude/issue-133-20260623-0941
fix: explicit UTF-8 encoding for text file reads on Windows (fixes #133)
2026-06-23 12:32:57 +01:00
Mervin Praison 1f887bd142 Merge pull request #148 from MervinPraison/claude/issue-134-20260623-0941
fix: doctor Gateway check uses /api/provider endpoint (fixes #134)
2026-06-23 12:32:50 +01:00
praisonai-triage-agent[bot]andMervin Praison 6facab2366 fix: use dynamic _icon() fallback for Windows-safe Rich output
Address Gemini reviewer feedback on the inconsistent hard-coded ASCII
replacements. Introduce _supports_unicode()/_icon() helpers that keep the
rich emoji UI on UTF-aware terminals (macOS/Linux/Windows Terminal) and
degrade to ASCII only on legacy code pages (cp1252/cp437), so output is
both crash-safe and free of UX regressions.

Co-authored-by: Mervin Praison <[email protected]>
2026-06-23 11:08:41 +00:00
praisonai-triage-agent[bot]andMervin Praison e9d5cff7db fix: doctor Gateway extractor reads flat /api/provider JSON (fixes #134)
The /api/provider endpoint flattens provider.health() into the top-level
response via info.update(health_data), so there is no nested "health"
object. The previous extractor read data.get("health") which was always
None, forcing status to "unknown" and the Gateway check to always "warn".
Read status/provider directly from the top-level dict.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>
2026-06-23 11:06:15 +00:00
praisonai-triage-agent[bot]andMervin Praison ff54833c84 fix: address review feedback on session search palette
- useSessionSearch: add `enabled` param so global Ctrl+K is only
  intercepted when the palette is rendered; case-insensitive key check
- ChatLayout: only enable the shortcut in fullscreen mode
- AgentUILayout: pass currentSessionId/onSessionChange to ChatArea so
  session selection is functional
- SessionSearch: guard async fetch with an active flag, hoist date
  objects out of the reduce loop, use a div for the loading state
  (CommandEmpty only renders post-filter)
- tests: patch async _datastore with AsyncMock and build the app via
  create_app() so the /sessions endpoint tests actually run

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>
2026-06-23 11:04:04 +00:00
MervinPraison 193dfcdef5 chore(ci): pin Claude Actions model to claude-opus-4-8 2026-06-23 11:39:36 +01:00
Mervin Praison 56181c8532 Merge pull request #130 from MervinPraison/claude/issue-127-20260617-0955
feat: implement exec approval modal (B-01) for OpenClaw UI parity
2026-06-23 11:39:10 +01:00
Mervin Praison 69461ebef3 Merge pull request #131 from MervinPraison/claude/issue-126-20260617
feat: wire i18n config to frontend runtime
2026-06-23 11:39:05 +01:00
Mervin Praison 7cc2031477 Merge pull request #143 from MervinPraison/claude/issue-137-fix-popstate
feat: fix browser Back/Forward navigation in docs SPA
2026-06-23 11:39:00 +01:00
Mervin Praison 9624f0b8df Merge pull request #144 from MervinPraison/claude/issue-139-20260623
feat: standardize health status checking to accept both 'ok' and 'healthy'
2026-06-23 11:38:55 +01:00
Mervin Praison 81aa5bf1e4 Merge pull request #141 from MervinPraison/claude/issue-136-health-alias
feat: add 'health' alias for 'health-check' command
2026-06-23 11:38:49 +01:00
Mervin Praison ec5d3087ab Merge pull request #142 from MervinPraison/claude/issue-135-health-performance
feat: optimize health endpoints for performance (fixes #135)
2026-06-23 11:38:43 +01:00
Mervin Praison d0e02dfd6d Merge pull request #129 from MervinPraison/claude/issue-125-remove-deprecated-dashboard
feat: remove deprecated 12-agent-dashboard example (XSS vulnerability)
2026-06-23 11:38:38 +01:00
Mervin Praison 3ad900a293 Merge pull request #140 from MervinPraison/claude/issue-138-version-sync
feat: sync package version from 0.3.114 to 0.3.121 (fixes #138)
2026-06-23 11:38:29 +01:00
praisonai-triage-agent[bot]andMervin Praison 963735699c refactor: eliminate code duplication in App.tsx navigation logic
- Extract findNavItemByPath as a reusable helper function
- Remove duplicate findItem function definitions
- Use consistent updateSEO function in both handleItemClick and popstate handler
- Move updateSEO function definition before its usage to fix reference order

This improves code maintainability by reducing duplication and ensuring
consistent SEO update behavior across all navigation paths.

Co-authored-by: Mervin Praison <[email protected]>
2026-06-23 10:16:56 +00:00
praisonai-triage-agent[bot]andMervin Praison e6e4c5a3e8 fix: extend health status standardization to features commands and test_runner
- Updated features list and features status commands to use is_success_status utility
- Updated test_runner health check to accept both 'ok' and 'healthy' statuses
- Added comprehensive unit tests for features commands with both status values
- Ensures consistent health status handling across entire CLI

Co-authored-by: Mervin Praison <[email protected]>
2026-06-23 10:16:09 +00:00
MervinPraison 2b9e78ea50 feat: standardize health status checking to accept both 'ok' and 'healthy' (fixes #139)
- Add health_utils.py with is_success_status() helper function
- Update CLI health-check command to treat both 'ok' and 'healthy' as success
- Update doctor command health extractor to use shared helper
- Update provider status command to use shared helper
- Add comprehensive unit tests for both status strings
- Document health status values in API documentation

The system now consistently accepts both 'ok' and 'healthy' as success
statuses, eliminating false alarms in CLI and doctor commands while
maintaining compatibility with existing dashboard behavior.
2026-06-23 09:51:59 +00:00
MervinPraisonandClaude de6d655250 feat: fix browser Back/Forward navigation in docs SPA (fixes #137)
- Fix popstate event handler to properly update route state when browser Back/Forward buttons are used
- Improve title template handling to support single %s placeholder in SEO config
- Add comprehensive tests for frontend navigation functionality
- Rebuild and update frontend bundle with the fixes

The popstate handler now correctly:
- Finds and sets the matching nav item based on URL path
- Updates both selectedItem and activeItemPath state
- Updates the document title using SEO template
- Handles root path navigation

🤖 Generated with Claude Code

Co-Authored-By: Claude <[email protected]>
2026-06-23 09:51:08 +00:00
MervinPraisonandClaude d9e1f9497d feat: optimize health endpoints for performance (fixes #135)
- Add /health/live for fast liveness checks (<100ms)
- Add /health/ready with parallel feature health checks (timeout: 500ms)
- Implement caching for /api/health endpoint (30s TTL)
- Maintain backward compatibility with existing /health endpoint
- Add comprehensive unit tests for all health endpoints
- Document deployment configurations for K8s, Docker, AWS

Addresses performance issue where /health was taking ~5s due to
sequential feature health checks. New endpoints provide:
- Liveness: <500ms p95 (typically <100ms)
- Readiness: <1s with parallel execution
- Cached: <50ms after first request

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-06-23 09:50:00 +00:00
praisonai-triage-agent[bot]andMervin Praison 5c3b93f452 fix: add explicit UTF-8 encoding to all text file reads for Windows compatibility
- Add encoding="utf-8" to all Path.read_text() calls
- Add encoding="utf-8" to all open() calls for text files
- Fixes UnicodeDecodeError on Windows where default encoding is cp1252
- Affects CLI commands, compiler, config store, and various features

Co-authored-by: Mervin Praison <[email protected]>
2026-06-23 09:48:31 +00:00
MervinPraisonandClaude 1e81b32f1d feat: add 'health' alias for 'health-check' command (fixes #136)
- Register 'health' as an alias for 'health-check' to support both command names
- Update documentation to use canonical 'health-check' with notes about 'health' alias
- Add tests to verify both commands work correctly

This ensures backward compatibility for users who expect 'aiui health' to work
while maintaining 'health-check' as the canonical command name.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-06-23 09:47:46 +00:00
MervinPraisonandClaude 862f0c4e5e feat: sync package version from 0.3.114 to 0.3.121 (fixes #138)
- Updated __version__.py to match pyproject.toml (0.3.121)
- Added unit test to enforce version consistency
- Ensures CLI, imports, and package metadata report same version

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-06-23 09:46:36 +00:00
praisonai-triage-agent[bot]andMervin Praison 27298ebfb2 fix: Windows CLI crashes with UnicodeEncodeError (fixes #132)
- Add Windows UTF-8 reconfiguration at module load
- Replace Unicode symbols with ASCII fallbacks in CLI output
- Update status icons to use [PASS]/[WARN]/[FAIL] instead of emojis
- Replace Unicode arrows and borders with ASCII characters
- Add comprehensive unit tests for Windows encoding

Co-authored-by: Mervin Praison <[email protected]>
2026-06-23 09:46:30 +00:00
praisonai-triage-agent[bot]andMervin Praison 33391d6775 fix: update doctor Gateway check to use existing /api/provider endpoint (fixes #134)
The doctor Gateway Status check was calling /api/provider/health which doesn't exist,
causing it to always show HTTP 404. Updated to use the existing /api/provider endpoint
and extract health data from the nested health object.

Co-authored-by: Mervin Praison <[email protected]>
2026-06-23 09:45:23 +00:00
praisonai-triage-agent[bot]andMervin Praison e1bb77ff1f fix(security): add input validation and XSS protection to i18n
- Add regex escaping in LocaleProvider to prevent regex injection
- Add locale format validation (e.g., 'en', 'en-US')
- Add key format validation for translation keys
- Pre-compile regex patterns for better performance
- Add AbortController to handle race conditions
- Add loading state to context for better UX

Co-authored-by: Mervin Praison <[email protected]>
2026-06-17 10:29:41 +00:00
praisonai-triage-agent[bot]andMervin Praison 4fe5667e06 fix: address architecture review findings for approval modal
- Fix RunEvent type safety by using RUN_PAUSED instead of invalid 'approval_required' string
- Optimize imports with module-level caching to avoid repeated imports on every tool call
- Update frontend to handle approval events via extra_data type checking
- Fix linting issues (whitespace in blank lines)
- Maintain backward compatibility with existing event handling

Import time remains under 200ms limit (178.5ms tested)

Co-authored-by: Mervin Praison <[email protected]>
2026-06-17 10:24:23 +00:00
MervinPraisonandClaude d9aaec3c1b feat: wire i18n config to frontend runtime (fixes #126)
- Promote i18n from EXPERIMENTAL to IMPLEMENTED status in feature registry
- Create LocaleProvider React context for managing locale state
- Add LocaleSwitcher component in header when multiple locales configured
- Connect frontend to existing i18n API endpoints for translation strings
- Implement RTL support with automatic dir attribute switching
- Add comprehensive tests for i18n implementation status
- Update compiler to remove experimental comment for i18n

This completes the i18n wiring gap identified in issue #126, enabling full
internationalization support in the frontend runtime with locale switching,
RTL support, and translation loading from the backend API.

Co-Authored-By: Claude <[email protected]>
2026-06-17 10:06:35 +00:00
praisonai-triage-agent[bot]andMervin Praison 92e257306f feat: add session search palette with Ctrl+K shortcut (fixes #128)
- Create SessionSearch component with command dialog UI
- Add useSessionSearch hook for keyboard shortcut handling
- Integrate into DashboardLayout, ChatLayout, and AgentUILayout
- Add fuzzy search by title and ID with grouping by date
- Include unit tests for session search functionality

Co-authored-by: Mervin Praison <[email protected]>
2026-06-17 10:06:06 +00:00
praisonai-triage-agent[bot]andMervin Praison 35a8030078 feat: implement exec approval modal for tool execution consent (fixes #127)
- Add approval modal system to dashboard with risk-based UI
- Integrate approval checking in provider for high-risk tools
- Connect SSE stream for real-time approval notifications
- Support approve/deny with optional 'always' policy
- Maintain import-time constraint under 200ms (186.8ms verified)

Co-authored-by: Mervin Praison <[email protected]>
2026-06-17 10:02:58 +00:00
MervinPraison 7ad2411cb5 feat: remove deprecated 12-agent-dashboard example (fixes #125)
Removes the deprecated examples/python/12-agent-dashboard/ directory
which contained XSS vulnerabilities via innerHTML usage. This example
was marked for removal in v0.5.0 due to unsafe rendering patterns.

- Deleted examples/python/12-agent-dashboard/ entirely
- Updated examples/README.md to redirect to secure alternatives (15-dashboard-test)
- Removed obsolete reference in 13-real-dashboard docstring

The example used innerHTML with API-derived data in 7+ locations,
creating stored XSS risks. Users should use examples 13 or 15 which
implement secure component-based rendering.
2026-06-17 09:59:49 +00:00
MervinPraisonandCursor 6b565c7210 fix(ci): use agent mode for workflow_dispatch Claude runs
Co-authored-by: Cursor <[email protected]>
2026-06-17 10:35:31 +01:00
MervinPraisonandCursor e67db90331 fix(ci): workflow_dispatch issue_number as string
Co-authored-by: Cursor <[email protected]>
2026-06-17 10:33:26 +01:00
MervinPraisonandCursor 1887ae6ae4 fix(ci): dispatch Claude directly from auto-issue-comment
GITHUB_TOKEN label events do not chain to other workflows; dispatch
claude.yml via workflow_dispatch after adding the claude label.

Co-authored-by: Cursor <[email protected]>
2026-06-17 10:32:02 +01:00
MervinPraisonandCursor ae919445a0 fix(ci): auto-escalate all new issues to Claude and use opus model
Enable issues:opened on auto-issue-comment.yml and set model: opus on
claude.yml to match praisonai-package.

Co-authored-by: Cursor <[email protected]>
2026-06-17 10:28:12 +01:00
MervinPraison 984ddf74d2 feat: add jobs proxy configuration for external job servers
- Introduced `set_jobs_proxy` function to allow forwarding of `/api/jobs/*` requests to an external jobs server.
- Updated documentation to reflect the new proxy setup and its usage.
- Enhanced server state reset to accommodate the new jobs API configuration.
- Added tests to ensure proper functionality of the jobs API configuration and proxy settings.

This change facilitates better integration when the dashboard and jobs server operate on different ports.
2026-06-17 10:20:55 +01:00
MervinPraison 5dd3cd81e4 docs: add A2UI canvas guide and surface API references
Document chat-canvas split preview, surface CLI, coerce_a2ui_tool_messages, and empty-canvas API behaviour; fix stale 29-a2ui-canvas paths.
2026-06-15 18:56:15 +01:00
a433c483c1 fix: document bootstrap entrypoints in examples (fixes #71) (#96)
* feat: standardize bootstrap entrypoint to 'aiui run app.py' across examples

Standardizes platform bootstrap patterns across examples per issue #71:

- Update examples README table to use 'aiui run app.py' consistently
- Update docstrings in examples 10, 12, 19-21 to show 'aiui run' instead of 'python app.py'
- Remove fragmented bootstrap approaches (uvicorn.run, asyncio patterns)
- Enforce single bootstrap SSOT as designed in CLI

Co-authored-by: Mervin Praison <[email protected]>

* feat: consolidate bootstrap entrypoints - auto-detect gateway apps (fixes #71)

- Add bootstrap detection in CLI run command
- Auto-detect gateway pattern (async main + AIUIGateway imports)
- Auto-switch to praisonai backend for gateway apps
- Delegate to user-defined main() when present
- Remove sys.path hacks from example 14
- Update examples README to show unified aiui run entrypoint
- Add BOOTSTRAP.md documentation for migration patterns

Co-authored-by: Mervin Praison <[email protected]>

* fix: resolve critical bootstrap entrypoint issues

- Fix AST parsing bug in gateway auto-detection (cli.py:1419)
- Revert background bots (email-bot, agentmail-bot) to python app.py
- Revert custom UI apps (agent-dashboard) to python app.py
- Update README.md to reflect correct run commands
- Fix import formatting and remove unused imports

These changes ensure that:
1. Gateway auto-detection works correctly by properly checking ast.alias.name
2. Background bots execute their __main__ blocks and don't start unnecessary web servers
3. Custom UI apps register their custom routes in __main__ blocks
4. Documentation matches actual working run commands

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-06-15 16:36:35 +01:00
814a2e1636 fix: honour zones.footer in FlexibleLayout runtime (fixes #63) (#89)
* feat: render footer zone in FlexibleLayout (fixes #63)

Adds missing footer zone rendering to inline FlexibleLayout case in App.tsx
to honor the zone schema contract. The footer zone was previously configured
in YAML but dropped at runtime due to incomplete implementation.

Co-authored-by: Mervin Praison <[email protected]>

* feat: add missing footer zone rendering to FlexibleLayout (fixes #63)

The inline FlexibleLayout implementation in App.tsx was missing the footer
zone rendering, causing zones.footer widgets to be schema-valid but
runtime-dropped. This adds the missing footer zone section to match the
proper FlexibleLayout component, ensuring footer widgets from examples
07-colorful and 08-corporate now render correctly.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <[email protected]>

* fix: add array length checks for all zone conditionals in FlexibleLayout

- Fix footer zone rendering to check both existence AND non-empty length
- Apply consistent fix to all zone conditionals (hero, leftSidebar, bottomNav, footer)
- Prevents rendering empty styled wrappers when zones array is empty but defined
- Addresses issue identified in Gemini Code Assist review

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: Claude <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-06-15 16:35:08 +01:00
01e6618833 fix: session memory governance in examples (fixes #67) (#92)
* feat: implement SessionGovernor for bounded session memory management (fixes #67)

- Add SessionGovernor with configurable max_turns/max_sessions limits
- Replace unbounded _contexts dict in example 03 with SessionGovernor
- Fix resource churn in examples 25/26 by reusing AsyncOpenAI client
- Implement documented --datastore json flag in example 04
- Export SessionGovernor in praisonaiui.__init__ for public API
- Enforce context window caps and LRU eviction policies
- Prevent OOM conditions in production deployments

Co-authored-by: Mervin Praison <[email protected]>

* fix: critical bugs in SessionGovernor memory management

Fixes two production-critical bugs in session_governor.py:

1. Line 199: Fixed eviction loop condition (>= → >) to prevent:
   - KeyError crashes when active session gets evicted
   - Unnecessary eviction when at limit
   - Double eviction causing under-capacity

2. Line 89-90: Fixed slicing logic for non-positive keep_count:
   - Added guard to prevent messages[-0:] returning all messages
   - Ensures proper memory governance when system messages exceed turn limits

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-15 16:34:23 +01:00
b3a5341785 fix: YAML profile semantic binding (fixes #61) (#88)
* feat: wire YAML profile selection to agent instructions (fixes #61)

- Modified _get_agent() in cli.py to read selected profile via get_selected_profile()
- Added profile-aware agent caching using profile ID as cache key
- Profiles can now include optional 'instructions' field for custom behavior
- Falls back to root-level instructions when profile has no specific instructions
- Maintains backward compatibility with existing YAML configs
- Resolves persona model split where UI profiles were cosmetic only

Co-authored-by: Claude <[email protected]>

* feat: fix persona model split - wire profile selection to agent behavior in YAML mode (fixes #61)

- Modified _get_agent() in cli.py to use selected profile for agent resolution
- Added support for per-profile instructions, model, and name fields in YAML
- Profile selection now creates distinct agent instances with different cache keys
- Updated example YAML files to demonstrate profile-specific instructions
- Added comprehensive unit tests validating the fix
- Maintains backward compatibility with existing configurations

Co-authored-by: Mervin Praison <[email protected]>

* fix: implement all gemini-code-assist defensive programming and test improvements

- Add defensive dict type checking in profile iteration (prevents AttributeError)
- Improve fallback logic using 'or' instead of dict.get() to handle None/empty values
- Enhance test coverage with actual behavioral validation instead of trivial assert

Fixes identified architectural issues for robust YAML profile handling.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
2026-06-15 16:33:57 +01:00
06ac1372eb fix: FlexibleLayout docs nav contract (fixes #56) (#84)
* feat: fix FlexibleLayout navigation invariant (fixes #56)

Implement runtime default navigation injection in FlexibleLayout case.
When zones.leftSidebar is not configured but navigation data exists,
render default Sidebar component to ensure docs sites remain navigable.

This fixes the layout subsystem contract violation where navigation
data was computed but not rendered, making docs sites unusable.

Co-authored-by: Mervin Praison <[email protected]>

* feat: implement navigation invariant in FlexibleLayout (fixes #56)

Fixes FlexibleLayout breaking docs site navigation contract by ensuring navigation
renders when content.docs exists, even without explicit leftSidebar zone config.

- Add navigation detection in FlexibleLayout case
- Render default Sidebar component when nav exists but no leftSidebar zone
- Preserve explicit zone override capability
- Fix ESLint error with proper case block scoping

Co-authored-by: Mervin Praison <[email protected]>

* fix: type safety violations in FlexibleLayout navigation invariant

- Use strict boolean expressions for zones and nav checks
- Add optional chaining for zones.leftSidebar access
- Fix TypeScript compilation under strict mode
- Addresses gemini-code-assist review feedback

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Mervin Praison <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-15 16:33:39 +01:00
7a320bde1b fix: surface platform-board integration errors (fixes #74) (#78)
* fix: surface platform-board integration errors instead of empty state (fixes #74)

Distinguish connection, auth, and HTTP failures from zero issues. Add
__main__ uvicorn bootstrap with HOST/PORT env overrides.

Co-authored-by: Cursor <[email protected]>

* fix: improve platform-board error handling

- Add explicit 200 status code check to prevent 3xx/204 from proceeding to JSON parsing
- Add safe JSON parsing with try/catch to handle invalid JSON responses
- Add defensive type checking for non-dict/non-list JSON responses

Addresses reviewer feedback from Gemini Code Assist review.

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-15 16:33:32 +01:00
2162b61629 fix: replace eval()-based calculate tool with safe math interpreter (fixes #60) (#76)
* fix: replace eval()-based calculate tool with safe math interpreter (fixes #60)

Add AST-walking eval_math_expression() with no eval/exec. Wire into YAML
calculate tool and the provider-praisonai example.

Co-authored-by: Cursor <[email protected]>

* fix: add exponent size limits to prevent DoS attacks in math evaluator

- Add validation to reject exponents with abs(exponent) > 100
- Prevents CPU/memory exhaustion via expressions like 2**1000000000
- Add comprehensive test coverage for exponent limits
- Addresses security concern identified in code review

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: MervinPraison <[email protected]>

---------

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
2026-06-15 16:33:25 +01:00
MervinPraisonandCursor e14b5817b1 fix(ci): restore missing uses in claude-response job
Co-authored-by: Cursor <[email protected]>
2026-06-15 16:26:34 +01:00
77d7d4241a fix: safe rendering in agent-dashboard (fixes #59) (#87)
* feat: deprecate unsafe innerHTML examples and fix XSS vulnerabilities (fixes #59)

- Add deprecation warning to example 12-agent-dashboard with runtime DeprecationWarning
- Replace innerHTML usage in plugin.js with safe DOM APIs (textContent, createElement)
- Example 12 now warns users to migrate to examples/15-dashboard-test/ or examples/13-real-dashboard/
- Prevents XSS injection through API responses in admin dashboard

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>

* fix: replace innerHTML with safe DOM methods to prevent XSS (fixes #59)

- Fixed renderApiPageData function XSS in error handling
- Fixed renderComponents function XSS in JSON viewer
- Fixed renderCard function XSS in title, value, footer fields
- Fixed renderChart function XSS in title field
- Fixed renderTable function XSS in headers and cell data
- Fixed aiui.createBoard error handler XSS in error messages
- Fixed navigation items XSS in page titles and icons
- Fixed header branding XSS in site title and logo
- Fixed A2UI surface fallback XSS in JSON output
- Fixed component default case XSS in JSON output
- Fixed key-value list XSS in labels and values

All innerHTML usage with user-controlled API data has been replaced
with safe DOM methods (createElement, textContent, appendChild) to
prevent XSS vulnerabilities in the admin dashboard.

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
2026-06-15 16:25:58 +01:00
MervinPraisonandCursor 86063a3ed0 fix(ci): checkout PR branch and require Claude to push fixes on final review
PR @claude runs now check out the PR head, set git identity, and use a
dedicated final-review prompt that forbids false fork excuses.

Co-authored-by: Cursor <[email protected]>
2026-06-15 16:25:05 +01:00
75fb931a23 docs: resolve tutorial namespace collisions (fixes #73) (#97)
* fix: update tutorial namespace references in examples

- Fixed stale chat-yaml-* references to use current yaml/* paths
- Updated 12-playground title to clarify agents style usage
- Ensures curriculum progression comments are accurate

Fixes #73

* fix: resolve namespace collisions in tutorial examples (fixes #73)

- Move yaml/02-clean-chat to yaml/03-clean-chat to resolve collision with yaml/02-profiles
- Move python/19-email-bot to python/20-email-bot to resolve collision with python/19-components-showcase
- Shift all subsequent numbered examples to maintain monotonic progression
- Update stale comments referencing chat-yaml-* namespaces to proper yaml/* paths
- Fix cross-references between YAML and Python examples

Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
2026-06-15 15:47:54 +01:00
7d098f4097 fix: composition wiring for corporate example (fixes #70) (#95)
* feat: add CompositionResolver for FlexibleLayout auto-bridging (fixes #70)

- Implement minimal CompositionResolver in compiler that auto-bridges registered
  components to appropriate zones for FlexibleLayout templates
- Auto-bridge components.sidebar → zones.leftSidebar
- Auto-bridge components.header → zones.header
- Auto-bridge components.footer → zones.footer
- Only applies to FlexibleLayout to preserve ThreeColumnLayout slot/ref paradigm
- Prevents duplicate components when already explicitly placed in zones
- Add comprehensive tests covering auto-bridging, layout specificity, and duplicates
- Fixes fractured composition model between slot/ref and zone paradigms
- Maintains <200ms import performance constraint

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>

* feat: implement CompositionResolver for component-zone auto-wiring (fixes #70)

- Add CompositionResolver to auto-wire components to FlexibleLayout zones
  - Maps sidebar → leftSidebar, header → header, footer → footer
  - Only wires when zone is empty and component exists
- Extend validator to detect orphaned components with auto-wiring support
  - Warns about components defined but never referenced
  - Excludes auto-wireable components for FlexibleLayout templates
- Fixes composition model fracture between component registry and zone system
- Maintains backward compatibility and import time <200ms

Co-authored-by: Mervin Praison <[email protected]>

* fix: consolidate CompositionResolver and fix validator bugs (addresses Gemini review feedback)

- Remove redundant _resolve_composition_wiring method
- Enhance _apply_composition_resolver to initialize template.zones when None
- Fix validator bug: compare widget types to component types, not names
- Fix test_no_auto_bridge_for_non_flexible_layout by adding dummy FlexibleLayout

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-06-15 15:47:33 +01:00
c752fe0f65 fix: tool capability honesty metadata (fixes #69) (#94)
* feat: add tool capability honesty layer for stub vs live tools (fixes #69)

- Add ToolCapability enum (STUB/LIVE) to classify tool behavior
- Mark web_search as STUB with upgrade_env for live data option
- Add calculate tool with capability metadata and eval warning
- Update API responses to include capability and upgrade_env fields
- Add frontend UI indicators: warning badges and expanded disclaimers
- Create get_stub_tool_disclaimer() for system prompt integration
- Maintain backward compatibility with existing tool definitions

Co-authored-by: Mervin Praison <[email protected]>

* fix: add defensive check to get_stub_tool_disclaimer

Address Gemini reviewer feedback by adding early return when
enabled_tools is None or empty, preventing potential runtime errors.

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-15 15:46:20 +01:00
c4384fd818 fix: thread-safe stream bridge for agent streaming (fixes #68) (#93)
* feat: add StreamBridge for thread-safe async streaming (fixes #68)

- Create StreamBridge abstraction in examples/_shared/ with thread-safe async-sync bridge
- Fix examples 07, 08, 13 to use call_soon_threadsafe instead of unsafe put_nowait
- Fix example 07 lifecycle ordering bug by concurrent event draining
- Add comprehensive unit tests for StreamBridge functionality
- All examples now use consistent thread-safe streaming pattern

Co-authored-by: Mervin Praison <[email protected]>

* fix: resolve thread-safety and performance issues in StreamBridge

- Initialize asyncio.Queue in __init__ to ensure thread-safe creation
- Remove race condition by eliminating lazy initialization
- Fix 1.0-second performance delay in provider example using sentinel pattern
- Remove unused imports and StreamBridge instance in provider
- Update tests to match new initialization behavior
- All tests pass and linting clean

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-15 15:46:15 +01:00
c8600029b3 feat: honest rename of blocks example to shadcn-deps (fixes #65) (#91)
Remove misleading 'Component Blocks Showcase' claims and orphaned /blocks/** route.
This example demonstrates dependency installation only, not component showcases.

- Renamed to 'shadcn Dependencies Installation Demo'
- Removed orphaned /blocks/** route
- Updated documentation to reflect actual functionality
- Example now honestly describes what it does: auto-downloads 22 shadcn components

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-15 15:46:10 +01:00
f56cbf6243 fix: compile enterprise config fields to frontend (fixes #64) (#90)
* feat: implement ConfigFeatureRegistry to fix schema-runtime gap

- Add ConfigFeatureRegistry to track implementation status of config fields
- Enhance validation with --strict flag warning about experimental fields
- Update compiler to emit seo/a11y configs to ui-config.json
- Add logoImage support in Header component
- SEO tags generation now uses configured title template and OG image
- Runtime warnings for experimental fields (i18n)
- Maintains backward compatibility with existing configs

Fixes #64

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>

* feat: fix multi-layer config contract - emit enterprise fields to frontend (fixes #64)

- Update compiler to emit seo, i18n, a11y, logo fields to ui-config.json
- Add TypeScript interfaces for enterprise config fields
- Enhance Header component to support logo configuration (light/dark variants)
- Add SkipLink component for accessibility with conditional rendering
- Enhance SEO meta tag generation with titleTemplate and Twitter config
- Add strict validation mode for experimental features
- All fields now flow properly: YAML → Pydantic → ui-config → Frontend

Co-authored-by: Mervin Praison <[email protected]>

* fix: address critical architecture and accessibility issues

- Fix double-templating bug in SEO title generation (compiler.py:581)
- Improve skip link accessibility by targeting Content component only
- Add logo fallback logic for missing theme variants
- Simplify title template parsing with sequential replace
- Correct CLI flag reference in validation error messages
- Exclude metadata fields from unimplemented feature validation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-06-15 15:46:04 +01:00
4fe69554fc fix: channel config secret classification (fixes #58) (#86)
* feat: implement channel config secret security validation (fixes #58)

- Add secret field detection to prevent inline API keys/passwords in channel configs
- Implement config validation that rejects secrets, requires env:VAR_NAME references
- Add response redaction to hide secrets from GET endpoints
- Update examples 20, 22 and seed data to use secure env: pattern
- Covers major secret types: api_key, bot_token, app_password, etc.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>

* fix: resolve _ref keys in bot startup and add defensive validation

- Add _get_config_secret() helper to resolve bot_token_ref, api_key_ref, app_token_ref
- Update _start_channel_bot() to use new helper for token resolution
- Add defensive input validation to _validate_config_security() and _redact_config_secrets()
- Fix defensive dictionary access in API endpoints (_list, _get)
- Update _resolve_slack_app_token() and test endpoint to use new helper

Fixes critical issue where bots configured with _ref keys could not start.

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-15 15:45:58 +01:00
9546cefbde fix: dedupe example 18 full-chat curriculum (fixes #57) (#85)
* feat: implement set_pages() in example 18 to fix curriculum duplication (fixes #57)

- Add aiui.set_pages(["chat", "sessions", "agents", "usage", "config"]) call
- This implements the page curation functionality promised in the docstring
- Resolves pedagogical architecture issue where example 18 was duplicate of 17
- Now example 18 genuinely teaches aiui.set_pages() API as advertised

Co-authored-by: Mervin Praison <[email protected]>

* feat: implement set_pages() in example 18 to demonstrate page curation

- Add aiui.set_pages() call to show only essential pages (chat, sessions, agents, usage, config)
- Update docstring to clarify difference from example 17 (all pages vs curated pages)
- Fix import ordering and remove unused imports per ruff
- Resolves curriculum duplication where 18 was identical to 17 but claimed different functionality

Fixes #57

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-15 15:45:53 +01:00
6a8c634031 fix: wire demo seed data into feature showcase (fixes #55) (#83)
* feat: restore demo data seeding in examples 10-12 (fixes #55)

- Update seed_demo_data() to delegate to seed_via_api() using TestClient
- Replaces no-op stub with working implementation
- Ensures examples show populated dashboards on startup
- Uses HTTP API as stable integration surface per architectural principle

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>

* feat: restore demo data seeding in examples 10-12 (fixes #55)

- Modified seed_demo_data() to call seed_via_api() with TestClient
- Preserves cold import performance with lazy imports (167ms < 200ms)
- Examples now show populated dashboards instead of empty tables
- Maintains backend compatibility via API-based seeding approach

Co-authored-by: Mervin Praison <[email protected]>

* fix: use TestClient as context manager for proper lifespan handling

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-15 15:45:47 +01:00
4df8c29f1d docs: sync examples/README.md with on-disk examples (fixes #62) (#80)
* docs: sync examples/README.md with on-disk Python and YAML examples (fixes #62)

Index all 35 Python apps, 13 YAML examples, and real site template folder
names. Note duplicate numeric prefixes and link to docs/examples guides.

Co-authored-by: Cursor <[email protected]>

* docs: fix playground run command to include --style agents flag

Fixes the run command for yaml/12-playground to match the docstring
in the YAML file itself, which specifies --style agents is required.

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-15 15:45:42 +01:00
e53df0ca78 fix: default python app.py examples to 127.0.0.1 (fixes #66) (#79)
* fix: default python app.py examples to 127.0.0.1 (fixes #66)

Bind loopback by default in 11 examples that call uvicorn or AIUIGateway
directly. HOST env var overrides for Docker/LAN demos.

Co-authored-by: Cursor <[email protected]>

* fix: make host variable consistent across all examples

Move HOST env var from global scope to local scope in main block
for consistency with other 10 examples.

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
2026-06-15 15:45:36 +01:00
3d9c8da13d docs: update extensibility README for set_custom_js API (fixes #72) (#77)
Remove stale DevTools workaround; document aiui.set_custom_js() loader
already used in app.py.

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-06-15 15:45:31 +01:00
b46f4450a0 fix(ci): add workflow permissions for PR comment triggers (#99)
GITHUB_TOKEN needs issues:write at workflow level for workflow_dispatch
and @claude comment posting.

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-06-15 15:10:11 +01:00
f4ec043989 fix(ci): enable PR auto-Claude final review chain (parity with PraisonAI) (#98)
- Fallback GH_TOKEN to github.token when user PAT missing
- Fix pull_request handler using wrong context.issue.number
- Add bot PR path: CodeRabbit → Claude directly (skip Copilot)
- Add workflow_dispatch to manually trigger Claude on any PR

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-06-15 15:07:35 +01:00
df02e0c15b fix(ci): dedupe Claude workflow when label and @claude comment both fire (#82)
Skip issue_comment trigger if claude label was added within 3 minutes.
Add per-issue concurrency to cancel overlapping runs.

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-06-15 13:44:08 +01:00
66acd35496 chore(ci): auto-trigger Claude on owner issue open (parity with PraisonAI) (#81)
* chore(ci): auto-trigger Claude on owner issue open (parity with PraisonAI)

Mirror praisonai-package claude.yml: triage labels on open, auto-add claude
label for owner/collaborator issues, GitHub App token, fork PR handling.

Co-authored-by: Cursor <[email protected]>

* chore(ci): add trigger workflow to sync secrets from PraisonAI

Co-authored-by: Cursor <[email protected]>

* fix(ci): fallback to GITHUB_TOKEN when Claude app secrets missing

Auto-triage works without cross-repo secret copy. Sync workflow documents
manual fallback when SECRETS_ADMIN_PAT is unavailable.

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-06-15 13:34:39 +01:00
MervinPraison 8a483a62dd chore: update dependencies and enhance CI workflow
- Updated `praisonaiagents` and `praisonai` versions in `pyproject.toml`.
- Added integration smoke tests to the CI workflow for better backend validation.
- Improved documentation on platform integration and SDK gaps.
- Enhanced the UI for issues and sessions with new toolbar components.
- Refactored backend integration checks and tool resolution logic for better maintainability.
2026-06-15 13:22:27 +01:00
MervinPraisonandCursor d418dcf443 refactor: harden input validation and access controls
Co-authored-by: Cursor <[email protected]>
2026-06-13 13:44:05 +01:00
Mervin Praison c467643c15 Merge pull request #53 from MervinPraison/feat/l1-integration-backends
feat: L1 backend integration for PraisonAI package
2026-05-21 11:59:15 +01:00
MervinPraison 8caa8d51d0 test: align unit tests with surface decorators, dashboard modules, MCP session context.
Update regression allowlist and assertions after integration branch API changes.
2026-05-21 11:58:00 +01:00
MervinPraison 541ada0399 chore: fix ruff lint errors for CI.
Remove unused imports and sort import blocks flagged by ruff.
2026-05-21 11:56:23 +01:00
MervinPraison fb28f2a2ee test(jobs): read jobs config from module after set_jobs_backend.
Imported dict binding was stale after global reassignment in set_jobs_api.
2026-05-21 11:55:10 +01:00
MervinPraison ac0a4f32a8 test(jobs): reset jobs API config before backend default assertion.
Avoid cross-test pollution when other suites call set_jobs_api first.
2026-05-21 11:52:29 +01:00
MervinPraison 3a0fa72c3d Merge origin/main into feat/l1-integration-backends and fix review issues.
Resolve pyproject/uv.lock version conflicts, merge package integration docs,
fix a2ui-mapper JS syntax, run workflows in thread pool, and use AsyncContext
for video agent tools when called from a running event loop.
2026-05-21 11:51:16 +01:00
MervinPraison f38aa27345 feat(jobs): add configurable jobs API and platform board examples.
Expose set_jobs_api/set_jobs_backend for dashboard jobs view routing and add platform-board examples with tests.
2026-05-21 11:45:45 +01:00
MervinPraison 39615089ba refactor(dashboard): slim view modules and add package alignment tests.
Delegate heavy dashboard views to shared modules, register shell header branding slot, and add tests for auth/api views and jobs UI config.
2026-05-21 11:45:36 +01:00
MervinPraison 77b67271ec feat(docs): add PraisonAI package integration documentation
This commit introduces detailed documentation on how to integrate
the PraisonAI package with the Agent runtime, gateway, and CLI.
It outlines deployment options, wiring checklists, API surfaces,
and plugin systems to facilitate user understanding and usage.

Additionally, updates the version in `pyproject.toml` and
`.gitignore` to include database files and modifies the lock file.
2026-05-21 11:44:05 +01:00
MervinPraison 7099e9f2ce u 2026-05-21 11:43:26 +01:00
MervinPraison 3f02c32740 feat(docs): add image preview feature for chat
Introduced documentation for a new feature allowing inline media rendering
of agent-generated images during chat. This update includes architecture
details, data flow paths, and examples for seamless integration with
existing backends. Updated related documentation to link to new feature.
2026-05-20 20:38:41 +01:00
MervinPraison a9dfc97e8c feat(chat): support inline media in chat messages
- Added support for inline media (images, videos, files) in chat messages
- Updated `ChatArea`, `ChatMessages`, and `ToolCallDisplay` components to render media
- Introduced `ImagePreviewProvider` example demonstrating dynamic image rendering
- Enhanced documentation on provider media capabilities
- Updated integration and unit tests to cover new media functionality
2026-05-20 20:03:01 +01:00
MervinPraison 5429a287e7 feat(frontend, docs): add A2UI surfaces and chat-canvas integration
- Implemented A2UI surfaces for agent-driven UI in chat/canvas.
- Enhanced documentation for architecture and agent UI features.
- Introduced new example demonstrating A2UI canvas integration.
- Updated various frontend components to support new features.
- Improved tests for A2UI and related functionalities.
2026-05-20 15:55:45 +01:00
MervinPraison a4167b1477 feat(video): add Video Studio feature with integration and examples
Introduced Video Studio integration with PraisonAI Video engine.
Added documentation, example app, and multiple backend routes for project management, scene processing, and rendering.
Updated version in pyproject.toml for release.
2026-05-20 12:11:11 +01:00
MervinPraison 0923aac53a feat(docs): add documentation for modular dashboard shell feature
This commit introduces comprehensive documentation for the Agent UI host, detailing the architecture, quick start guide, extension API, layout components, optional dashboard modules, and third-party plugin development. Additionally, it includes a minimal example of an external agent hosted on the PraisonAIUI dashboard shell.
2026-05-19 21:27:40 +01:00
Mervin PraisonandMervinPraison b589c6fce2 refactor(api): PraisonAI-native names for discovery, citations, reasoning types (#52)
Rename the six public dataclasses shipped in v0.3.113 so none of them are
exact-name lifts from an external framework.  Names are now PraisonAI-unique,
noun-first, non-developer-friendly, and pair with the existing ``*Prompt`` /
``ThinkingSteps`` PraisonAI style.

## Rename table

| v0.3.113 (lifted) | v0.3.114 (PraisonAI-native) |
|-------------------|-----------------------------|
| ModelInfo         | **ModelCard**               |
| AgentDetails      | **AgentCard**               |
| TeamDetails       | **TeamCard**                |
| Reference         | **SourceChunk**             |
| ReferenceData     | **SourceBundle**            |
| ReasoningStep     | **ThoughtStep**             |
| `references_event()`     | **`source_bundle_event()`** |
| `reasoning_step_event()` | **`thought_step_event()`**  |

## What stays the same

- **Wire format is unchanged.**  The SSE event ``type`` string is still
  ``"references"``, and the JSON payload still uses the keys ``query``,
  ``references``, ``time_ms`` so third-party chat frontends see the exact
  shape they expect.
- ``RunEvent`` and ``RunEventType`` keep their existing names — these
  pre-existed in PraisonAIUI before the competitor audit and are not lifts.
- ``RunEventType.REFERENCES = "references"`` enum variant is unchanged.
- All REST routes added in v0.3.113 are unchanged (``/teams``, etc.).

## Why a breaking rename (no aliases)

Keeping the lifted names even as silent aliases would defeat the stated
goal ("names unique for PraisonAI").  v0.3.113 was released <1 h before
this rename, so no external consumer can have taken a dependency yet.

## Tests

- Every existing test in ``tests/unit/test_protocol_parity.py`` updated
  to the new names — **all 15 pass**.
- **New regression guard** ``test_no_competitor_lifted_names_exported``
  fails loudly if any of the six lifted names ever reappear on the
  public module, preventing accidental reintroduction.

Full suite: **955 pass**, 4 skipped, 8 xfailed, 1 xpassed.  Ruff clean.

## Version
Bumps to 0.3.114.

Co-authored-by: MervinPraison <[email protected]>
2026-04-23 00:41:32 +01:00
546 changed files with 49614 additions and 6366 deletions

No files matched your search

+17 -1
View File
@@ -138,7 +138,23 @@ Final rule: conclude only when evidence shows missing = 0.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PUBLISH WORKFLOW (reference)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 1 — Publish praisonaiagents (Core SDK):
GitHub Actions (recommended):
GitHub → Actions → "PyPI Release" → Run workflow → bump: patch
Requires repo secrets: PYPI_TOKEN, GH_TOKEN
Requires GitHub Environment "pypi" with required reviewers (Settings → Environments)
Manual — Publish aiui (PraisonAIUI):
cd /Users/praison/PraisonAIUI
# Bump version in pyproject.toml and src/praisonaiui/__version__.py
rm -rf dist && uv lock && uv build && uv publish --token "$PYPI_TOKEN"
git add pyproject.toml uv.lock src/praisonaiui/__version__.py
git commit -m "Release vX.Y.Z" && git tag vX.Y.Z && git push && git push --tags
gh release create vX.Y.Z --title "PraisonAIUI vX.Y.Z" --notes "Release vX.Y.Z" --latest
PyPI package name: aiui
Verify: pip index versions aiui | head -1
PraisonAI stack (separate repo — publish SDK before wrapper):
cd /Users/praison/praisonai-package/src/praisonai-agents
praisonai publish pypi
# Uses uv internally: uv lock → uv build → uv publish
@@ -0,0 +1,290 @@
name: "Claude Code Action"
description: "Run Claude Code in GitHub Actions workflows"
inputs:
anthropic_api_key:
description: "Anthropic API key"
required: false
claude_code_oauth_token:
description: "Claude Code OAuth Token for Claude Pro"
required: false
github_token:
description: "GitHub token for Claude to operate with"
required: false
default: ${{ github.token }}
trigger_phrase:
description: "The trigger phrase to look for in comments, issue/PR bodies, and issue titles"
required: false
default: "@claude"
assignee_trigger:
description: "The assignee username that triggers the action (e.g. @claude). Only used for issue assignment"
required: false
max_turns:
description: "Maximum number of conversation turns Claude can take"
required: false
timeout_minutes:
description: "Timeout in minutes for execution"
required: false
default: "30"
model:
description: "Model to use (provider-specific format required for Bedrock/Vertex)"
required: false
use_bedrock:
description: "Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API"
required: false
default: "false"
use_vertex:
description: "Use Google Vertex AI with OIDC authentication instead of direct Anthropic API"
required: false
default: "false"
allowed_tools:
description: "Additional tools for Claude to use (the base GitHub tools will always be included)"
required: false
default: ""
disallowed_tools:
description: "Tools that Claude should never use"
required: false
default: ""
custom_instructions:
description: "Additional custom instructions to include in the prompt for Claude"
required: false
default: ""
mcp_config:
description: "Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers"
required: false
default: ""
claude_env:
description: "Custom environment variables to pass to Claude Code execution (YAML format)"
required: false
default: ""
direct_prompt:
description: "Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows)"
required: false
runs:
using: "composite"
steps:
- name: Install Claude Code
shell: bash
run: npm install -g @anthropic-ai/claude-code
- name: Install GitHub MCP Server
shell: bash
run: |
claude mcp add-json github '{
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server:sha-ff3036d"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ inputs.github_token }}"
}
}'
- name: Extract GitHub Context and Create Prompt
shell: bash
id: prepare_context
env:
# Pass user-controlled inputs via environment variables to prevent script injection (GHSL-2025-093)
EVENT_COMMENT_BODY: ${{ github.event.comment.body }}
EVENT_ISSUE_TITLE: ${{ github.event.issue.title }}
EVENT_ISSUE_BODY: ${{ github.event.issue.body }}
EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
EVENT_PR_TITLE: ${{ github.event.pull_request.title }}
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
EVENT_REVIEW_BODY: ${{ github.event.review.body }}
EVENT_ASSIGNEE_LOGIN: ${{ github.event.assignee.login }}
INPUT_TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
INPUT_ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
INPUT_DIRECT_PROMPT: ${{ inputs.direct_prompt }}
EVENT_ACTION: ${{ github.event.action }}
EVENT_NAME: ${{ github.event_name }}
REPO_NAME: ${{ github.repository }}
run: |
echo "🔍 Extracting GitHub context from event: $EVENT_NAME"
# Function to check for trigger phrase
check_trigger() {
local text="$1"
local trigger="$INPUT_TRIGGER_PHRASE"
if [[ "$text" == *"$trigger"* ]]; then
return 0
fi
return 1
}
# Extract context based on event type
TRIGGER_FOUND="false"
USER_REQUEST=""
CONTEXT_INFO=""
case "$EVENT_NAME" in
"issue_comment")
COMMENT_BODY="$EVENT_COMMENT_BODY"
ISSUE_TITLE="$EVENT_ISSUE_TITLE"
ISSUE_NUMBER="$EVENT_ISSUE_NUMBER"
if check_trigger "$COMMENT_BODY"; then
TRIGGER_FOUND="true"
USER_REQUEST="$COMMENT_BODY"
CONTEXT_INFO="Issue Comment on #$ISSUE_NUMBER: $ISSUE_TITLE"
fi
;;
"pull_request_review_comment")
COMMENT_BODY="$EVENT_COMMENT_BODY"
PR_TITLE="$EVENT_PR_TITLE"
PR_NUMBER="$EVENT_PR_NUMBER"
if check_trigger "$COMMENT_BODY"; then
TRIGGER_FOUND="true"
USER_REQUEST="$COMMENT_BODY"
CONTEXT_INFO="PR Comment on #$PR_NUMBER: $PR_TITLE"
fi
;;
"pull_request_review")
REVIEW_BODY="$EVENT_REVIEW_BODY"
PR_TITLE="$EVENT_PR_TITLE"
PR_NUMBER="$EVENT_PR_NUMBER"
if check_trigger "$REVIEW_BODY"; then
TRIGGER_FOUND="true"
USER_REQUEST="$REVIEW_BODY"
CONTEXT_INFO="PR Review on #$PR_NUMBER: $PR_TITLE"
fi
;;
"issues")
ISSUE_BODY="$EVENT_ISSUE_BODY"
ISSUE_TITLE="$EVENT_ISSUE_TITLE"
ISSUE_NUMBER="$EVENT_ISSUE_NUMBER"
if check_trigger "$ISSUE_TITLE" || check_trigger "$ISSUE_BODY"; then
TRIGGER_FOUND="true"
USER_REQUEST="$ISSUE_BODY"
CONTEXT_INFO="Issue #$ISSUE_NUMBER: $ISSUE_TITLE"
elif [[ "$EVENT_ACTION" == "assigned" && -n "$INPUT_ASSIGNEE_TRIGGER" ]]; then
ASSIGNEE="$EVENT_ASSIGNEE_LOGIN"
if [[ "$ASSIGNEE" == "$INPUT_ASSIGNEE_TRIGGER" ]]; then
TRIGGER_FOUND="true"
USER_REQUEST="$ISSUE_BODY"
CONTEXT_INFO="Issue #$ISSUE_NUMBER assigned to $ASSIGNEE: $ISSUE_TITLE"
fi
fi
;;
esac
# Check for direct prompt override
if [[ -n "$INPUT_DIRECT_PROMPT" ]]; then
TRIGGER_FOUND="true"
USER_REQUEST="$INPUT_DIRECT_PROMPT"
CONTEXT_INFO="Automated GitHub workflow"
fi
if [[ "$TRIGGER_FOUND" != "true" ]]; then
echo "❌ No trigger phrase found or direct prompt provided. Exiting gracefully."
echo "SKIP_EXECUTION=true" >> $GITHUB_ENV
exit 0
fi
echo "✅ Trigger found! Context: $CONTEXT_INFO"
# Create comprehensive prompt
mkdir -p /tmp/claude-action
cat > /tmp/claude-action/github-context-prompt.txt << EOF
You are Claude Code, an AI assistant helping with GitHub workflows and code.
Repository: $REPO_NAME
Context: $CONTEXT_INFO
Event: $EVENT_NAME
User Request:
$USER_REQUEST
Please analyze the request and provide helpful assistance.
CRITICAL INSTRUCTION: If your task involves modifying codebase files to add a feature or fix a bug:
STEP 0 — SETUP GIT IDENTITY:
git config user.name "MervinPraison"
git config user.email "[email protected]"
STEP 1 — READ GUIDELINES:
MANDATORY: Before writing any code, you MUST carefully read "src/praisonai-agents/AGENTS.md" to understand this repository's extremely specific Architecture, Protocol-First design rules, and Agent-Centric Philosophy.
STEP 2 — ARCHITECTURE VALIDATION & ROUTING (MANDATORY before writing code):
Before implementing anything, answer these questions:
- CORE vs WRAPPER vs TOOLS ROUTING:
1. Core SDK (praisonaiagents/): Only core protocols, base classes, decorators. No heavy implementations.
2. Wrapper (praisonai/): CLI, heavy implementations, optional dependencies.
3. Tools (PraisonAI-Tools): Specific integrations, external tools (e.g. SurrealDB, Slack), and community extensions MUST go to the `MervinPraison/PraisonAI-Tools` repository. If the feature is a tool, clone `https://github.com/MervinPraison/PraisonAI-Tools`, implement it there, and create the PR using `gh pr create -R MervinPraison/PraisonAI-Tools`.
- Does it duplicate existing functionality? Check if Agent already supports this via existing params (reflection, planning, tools, hooks, memory).
- Does it inherit from Agent properly? New agent types MUST inherit Agent, not wrap it with composition.
- Does it add new dependencies? Only optional deps allowed, must be lazy-imported.
- Will agent.py grow larger? If the change adds >50 lines to agent.py, find a way to extract instead.
- Is there a name collision with existing exports in __init__.py?
If ANY of these conceptual checks fail (excluding routing), add a comment to the issue explaining why and close it. Do NOT create a PR.
STEP 3 — IMPLEMENT:
- Create a new git branch appropriately named (e.g. \`feature/issue-<number>\`)
- Follow protocol-driven design: protocols in core SDK, heavy implementations in wrapper
- Keep changes small and backward-compatible
STEP 4 — TEST:
- Run: cd src/praisonai-agents && PYTHONPATH=. python -m pytest tests/ -x -q --timeout=30
- Ensure no regressions
STEP 5 — CREATE PR:
- Commit with descriptive message, push the branch
- Use the GitHub CLI (\`gh pr create --title "..." --body "Fixes #<number>"\`) to open a Pull Request
CRITICAL: You MUST create the PR automatically using \`gh pr create\`. Do NOT just provide a link.
Respond naturally and helpfully to the user's request using the available tools.
EOF
echo "PROMPT_FILE=/tmp/claude-action/github-context-prompt.txt" >> $GITHUB_ENV
echo "SKIP_EXECUTION=false" >> $GITHUB_ENV
- name: Run Claude Code
if: env.SKIP_EXECUTION != 'true'
shell: bash
run: |
echo "🚀 Running Claude Code with GitHub context..."
# Build command arguments
CMD_ARGS=("-p" "--verbose" "--output-format" "stream-json")
# Add max turns if specified
if [[ -n "${{ inputs.max_turns }}" ]]; then
CMD_ARGS+=("--max-turns" "${{ inputs.max_turns }}")
fi
# Add allowed tools (include GitHub tools by default)
TOOLS="mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues,mcp__github__create_comment,Read,Write,Edit,Bash"
if [[ -n "${{ inputs.allowed_tools }}" ]]; then
TOOLS="$TOOLS,${{ inputs.allowed_tools }}"
fi
CMD_ARGS+=("--allowedTools" "$TOOLS")
# Add disallowed tools
if [[ -n "${{ inputs.disallowed_tools }}" ]]; then
CMD_ARGS+=("--disallowedTools" "${{ inputs.disallowed_tools }}")
fi
echo "📝 Executing Claude Code with prompt from file..."
# Execute Claude Code with timeout, using stdin for the prompt
TIMEOUT_SECONDS=$((${{ inputs.timeout_minutes }} * 60))
timeout $TIMEOUT_SECONDS claude "${CMD_ARGS[@]}" < "${{ env.PROMPT_FILE }}"
echo "✅ Claude Code execution completed"
env:
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
GITHUB_TOKEN: ${{ inputs.github_token }}
@@ -0,0 +1,91 @@
name: "Claude Issue Triage Action"
description: "Automatically triage GitHub issues using Claude Code"
inputs:
timeout_minutes:
description: "Timeout in minutes for execution"
required: false
default: "5"
claude_code_oauth_token:
description: "Claude Code OAuth token for Pro subscription access"
required: true
github_token:
description: "GitHub token with repo and issues permissions"
required: true
runs:
using: "composite"
steps:
- name: Checkout repository code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create prompt
id: generate_prompt
shell: bash
run: |
mkdir -p /tmp/claude-prompts
cat > /tmp/claude-prompts/claude-issue-triage-prompt.txt << 'EOF'
You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels.
Issue Information:
- REPO: ${{ github.repository }}
- ISSUE_NUMBER: ${{ github.event.issue.number }}
TASK OVERVIEW:
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
2. Next, use the GitHub tools to get context about the issue:
- You have access to these tools:
- mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
- mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
- mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
- mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
- mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
- Start by using mcp__github__get_issue to get the issue details
3. Analyze the issue content, considering:
- The issue title and description
- The type of issue (bug report, feature request, question, etc.)
- Technical areas mentioned
- Severity or priority indicators
- User impact
- Components affected
4. Select appropriate labels from the available labels list provided above:
- Choose labels that accurately reflect the issue's nature
- Be specific but comprehensive
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
- Consider platform labels (android, ios) if applicable
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
5. Apply the selected labels:
- Use mcp__github__update_issue to apply your selected labels
- DO NOT post any comments explaining your decision
- DO NOT communicate directly with users
- If no labels are clearly applicable, do not apply any labels
IMPORTANT GUIDELINES:
- Be thorough in your analysis
- Only select labels from the provided list above
- DO NOT post any comments to the issue
- Your ONLY action should be to apply labels using mcp__github__update_issue
- It's okay to not add any labels if none are clearly applicable
EOF
echo "PROMPT<<DELIMITER" >> $GITHUB_OUTPUT
cat /tmp/claude-prompts/claude-issue-triage-prompt.txt >> $GITHUB_OUTPUT
echo "DELIMITER" >> $GITHUB_OUTPUT
- name: Run Claude Code
uses: ./.github/actions/claude-code-action
with:
direct_prompt: ${{ steps.generate_prompt.outputs.PROMPT }}
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
timeout_minutes: ${{ inputs.timeout_minutes }}
claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }}
github_token: ${{ inputs.github_token }}
+9
View File
@@ -0,0 +1,9 @@
{
"templateRepo": "MervinPraison/github-automation-template",
"stack": "claude",
"profile": "sdk",
"mode": "hub",
"hubRef": "v1.1.6",
"templateVersion": "1.1.6",
"installedAt": "2026-09-07T10:02:11Z"
}
+153
View File
@@ -0,0 +1,153 @@
/**
* Idempotent CodeRabbit/Qodo kick for bot-opened PRs.
* @see .github/workflows/auto-pr-comment.yml, claude.yml, bot-pr-recovery.yml
*/
const KICK_AUTHORS = new Set(['MervinPraison', 'github-actions[bot]']);
const BOT_PR_AUTHORS = new Set(['praisonai-triage-agent[bot]', 'github-actions[bot]']);
async function listAllComments(github, owner, repo, issueNumber) {
if (typeof github.paginate === 'function') {
return github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
}
const { data } = await github.rest.issues.listComments({
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
return data;
}
function kickAuthored(comment, marker) {
return (
KICK_AUTHORS.has(comment.user?.login) &&
(comment.body || '').includes(marker)
);
}
function coderabbitKickPosted(comments) {
return comments.some((c) => kickAuthored(c, '@coderabbitai review'));
}
function qodoKickPosted(comments) {
return comments.some((c) => kickAuthored(c, '/review'));
}
function chainKickPosted(comments) {
return coderabbitKickPosted(comments) && qodoKickPosted(comments);
}
function isBotOpenedPr(pr) {
if (BOT_PR_AUTHORS.has(pr.user?.login)) return true;
return pr.user?.type === 'Bot';
}
async function kickReviewChain(github, owner, repo, prNumber, core, preFetchedComments = null) {
const comments = preFetchedComments || await listAllComments(github, owner, repo, prNumber);
const needCoderabbit = !coderabbitKickPosted(comments);
const needQodo = !qodoKickPosted(comments);
if (!needCoderabbit && !needQodo) {
core?.info?.(`Review chain already kicked on PR #${prNumber}`);
return { kicked: false, reason: 'already_kicked' };
}
if (needCoderabbit) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: '@coderabbitai review',
});
}
if (needQodo) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: '/review',
});
}
core?.info?.(`Kicked review chain for PR #${prNumber} (coderabbit=${needCoderabbit}, qodo=${needQodo})`);
return { kicked: true, coderabbit: needCoderabbit, qodo: needQodo };
}
async function findOpenPrForIssue(github, owner, repo, issueNumber) {
const prefix = `claude/issue-${issueNumber}-`;
const { data: prs } = await github.rest.pulls.list({
owner,
repo,
state: 'open',
sort: 'created',
direction: 'desc',
per_page: 30,
});
return (
prs.find(
(p) => (p.head?.ref || '').startsWith(prefix) && isBotOpenedPr(p)
) || null
);
}
async function kickReviewChainForIssue(github, owner, repo, issueNumber, core) {
const pr = await findOpenPrForIssue(github, owner, repo, issueNumber);
if (!pr) {
core?.info?.(`No open PR for issue #${issueNumber}, skipping review kick`);
return { kicked: false, reason: 'no_pr' };
}
const result = await kickReviewChain(github, owner, repo, pr.number, core);
return { ...result, prNumber: pr.number };
}
async function recoverStalledBotPrs(github, owner, repo, options, core) {
const { prNumber = null, minAgeMs = 10 * 60 * 1000, maxRecover = 10 } = options || {};
let prs;
if (prNumber) {
const { data } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
prs = [data];
} else if (typeof github.paginate === 'function') {
prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
per_page: 100,
});
} else {
const { data } = await github.rest.pulls.list({ owner, repo, state: 'open', per_page: 100 });
prs = data;
}
const cutoff = Date.now() - minAgeMs;
let recovered = 0;
for (const pr of prs) {
if (recovered >= maxRecover) break;
if (!isBotOpenedPr(pr)) continue;
if (!prNumber && new Date(pr.created_at).getTime() > cutoff) continue;
const comments = await listAllComments(github, owner, repo, pr.number);
if (chainKickPosted(comments)) continue;
await kickReviewChain(github, owner, repo, pr.number, core, comments);
recovered += 1;
}
core?.info?.(`Recovery complete (${recovered} PR(s) kicked)`);
return recovered;
}
module.exports = {
KICK_AUTHORS,
BOT_PR_AUTHORS,
listAllComments,
chainKickPosted,
coderabbitKickPosted,
qodoKickPosted,
isBotOpenedPr,
kickReviewChain,
findOpenPrForIssue,
kickReviewChainForIssue,
recoverStalledBotPrs,
};
@@ -0,0 +1,44 @@
#!/usr/bin/env node
/**
* Run: node .github/scripts/ci-failure-claude-selftest.js
*/
const ciFix = require('./ci-failure-claude.js');
const mergeGate = require('./merge-gate.js');
const config = require('./gate-config.js');
let failed = 0;
function assert(name, cond) {
if (!cond) {
console.error('FAIL:', name);
failed += 1;
} else {
console.log('ok:', name);
}
}
const LOG = `
python UNKNOWN STEP FAILED (0.0100s) tests/unit/test_example.py::test_foo - AssertionError: bar
python UNKNOWN STEP ##[error]Process completed with exit code 1.
`;
const parsed = ciFix.parsePytestFailures(LOG);
assert('parses pytest failure', parsed.length === 1);
assert('uses CI workflow list', config.ciFailureWorkflowRuns.includes('CI'));
const comment = ciFix.buildCiFixComment({
headSha: 'abc1234567890abcdef1234567890abcdef12',
failedChecks: [{ name: 'python', workflow: 'CI', html_url: 'https://example.com/job/1' }],
failureSummaries: [{ jobName: 'python', failures: parsed }],
});
assert('comment mentions product guardrails', comment.includes('Product guardrails'));
const skip = ciFix.shouldSkipCiFix({
comments: [],
headSha: 'abc1234567890abcdef1234567890abcdef12',
labels: [],
claudeInProgress: false,
failedChecks: [{ name: 'python', workflow: 'CI' }],
});
assert('ci fix without FINAL review allowed', !skip.skip);
process.exit(failed ? 1 : 0);
+293
View File
@@ -0,0 +1,293 @@
/**
* Post @claude comments with CI failure details for internal PRs.
* @see .github/workflows/ci-failure-claude.yml
*/
const mergeGate = require('./merge-gate.js');
const config = require('./gate-config.js');
const CI_FIX_LABEL = 'claude-ci-fix-pending';
const COOLDOWN_MS = 12 * 60 * 60 * 1000;
const AUTO_ACTORS = mergeGate.AUTO_ACTORS;
const MAX_FAILURES = 15;
const MAX_JOBS_TO_FETCH = 5;
function shortSha(headSha) {
return (headSha || '').slice(0, 8);
}
function ciFixShaMarker(headSha) {
return `ci failed on head \`${shortSha(headSha)}`.toLowerCase();
}
function isCiFixComment(comment) {
const body = (comment.body || '').toLowerCase();
return body.includes('@claude') && body.includes('ci failed on head');
}
function hasCiFixCommentForSha(comments, headSha) {
const marker = shortSha(headSha).toLowerCase();
return comments.some((c) => {
if (!AUTO_ACTORS.includes(c.user.login)) return false;
if (!isCiFixComment(c)) return false;
return (c.body || '').toLowerCase().includes(marker);
});
}
function hasRecentCiFixComment(comments, headSha) {
const cutoff = Date.now() - COOLDOWN_MS;
const marker = shortSha(headSha).toLowerCase();
return comments.some((c) => {
if (!AUTO_ACTORS.includes(c.user.login)) return false;
if (!isCiFixComment(c)) return false;
const body = (c.body || '').toLowerCase();
if (body.includes(marker)) return true;
return new Date(c.created_at).getTime() > cutoff;
});
}
function shouldSkipCiFix({ comments, headSha, labels, claudeInProgress, failedChecks }) {
if (!failedChecks.length) return { skip: true, reason: 'no failed checks' };
if (labels.includes(CI_FIX_LABEL)) return { skip: true, reason: 'ci fix pending' };
if (claudeInProgress) return { skip: true, reason: 'claude in progress' };
if (hasCiFixCommentForSha(comments, headSha)) {
return { skip: true, reason: 'already commented for this sha' };
}
if (hasRecentCiFixComment(comments, headSha)) {
return { skip: true, reason: 'recent ci fix comment cooldown' };
}
return { skip: false };
}
function parsePytestFailures(logText) {
if (!logText) return [];
const lines = logText.split('\n');
const failures = [];
const seen = new Set();
for (let i = lines.length - 1; i >= 0 && failures.length < MAX_FAILURES; i -= 1) {
const line = lines[i].trim();
if (!line) continue;
const pytestMatch = line.match(
/FAILED\s*(?:\([^)]+\))?\s+(tests\/[^\s]+(?:::[^\s-]+)*)\s*-\s*(.+)/i
);
if (pytestMatch) {
const testId = pytestMatch[1];
if (!seen.has(testId)) {
seen.add(testId);
failures.push({ testId, error: pytestMatch[2].trim() });
}
continue;
}
const errorMatch = line.match(/##\[error\](.+)/);
if (errorMatch && /tests\//.test(errorMatch[1])) {
const msg = errorMatch[1].trim();
if (!seen.has(msg)) {
seen.add(msg);
failures.push({ testId: msg, error: msg });
}
}
}
return failures;
}
async function fetchJobFailureSummary(github, owner, repo, jobId, jobName) {
try {
const response = await github.rest.actions.downloadJobLogsForWorkflowRun({
owner,
repo,
job_id: jobId,
});
let logText = typeof response.data === 'string' ? response.data : String(response.data || '');
if (logText.length > 100000) {
logText = logText.slice(-100000);
}
return { jobName, jobId, failures: parsePytestFailures(logText) };
} catch (err) {
return { jobName, jobId, failures: [], error: err.message };
}
}
function mapFailedChecks(runs) {
return runs.map((run) => ({
name: run.name,
id: run.id,
html_url: run.html_url || run.details_url,
workflow: run.app?.slug || run.check_suite?.app?.slug || run.name,
}));
}
function buildCiFixComment({ headSha, failedChecks, failureSummaries }) {
const parts = [
`@claude CI failed on HEAD \`${shortSha(headSha)}\`. Please fix the failures below and push to this branch.`,
'',
'## Failed checks',
];
for (const check of failedChecks) {
parts.push(`- **${check.workflow || check.name}** / \`${check.name}\` — ${check.html_url}`);
}
parts.push('', '## Failures (extracted)');
let idx = 0;
for (const summary of failureSummaries) {
for (const failure of summary.failures) {
idx += 1;
if (idx > MAX_FAILURES) break;
parts.push(`${idx}. \`${failure.testId}\` — \`${failure.error}\``);
parts.push(` - Job: \`${summary.jobName}\``);
}
if (idx >= MAX_FAILURES) break;
}
if (idx === 0) {
parts.push('_(Could not extract pytest details — see job logs above.)_');
}
const exampleTest = failureSummaries.find((s) => s.failures.length)?.failures[0]?.testId;
parts.push(
'',
'## Critical review first',
'Before changing code or tests, decide **which side is wrong**:',
'1. **Legitimate feature change** — the PR intent is correct but implementation or tests need updating. Preserve product guarantees; update tests only when behaviour intentionally changed and document why.',
'2. **Regression / bug in this PR** — the failure exposes a real breakage introduced here. Fix the implementation; **do not weaken, skip, or delete tests** just to go green.',
'3. **Pre-existing flake or unrelated failure** — say so explicitly; prefer fixing the root cause over masking it.',
'',
'**Product guardrails (AGENTS.md):**',
'- Do not disturb PraisonAIUI contracts or frontend/backend layering to accommodate scope creep.',
'- Core agent runtime belongs in praisonaiagents; agent-callable tools belong in PraisonAI-Tools — not here.',
'- Prefer existing YAML config, plugins, and feature modules over new server.py/create_app() params or knobs with no live consumer.',
'- Tests must continue to guard backward compatibility and UX behaviour — passing CI by lowering test standards is not acceptable.',
'- If the feature does not genuinely add product value, recommend reverting or narrowing scope instead of patching around failures.',
'',
'## What to do',
'1. State your verdict: **legitimate fix**, **regression fix**, or **needs human review** — and why (1–3 sentences).',
'2. Fix root cause with **minimal changes**; never bloat server.py or create_app() with extra params.',
exampleTest
? `3. Run failing tests locally, e.g. \`pytest ${exampleTest} -q\`, plus any related tests touched by the PR.`
: '3. Run failing tests locally with targeted pytest, plus any related tests touched by the PR.',
'4. Push to this branch and comment: files changed, review verdict, and why tests still protect product behaviour.',
);
return parts.join('\n');
}
async function maybeClearCiFixLabel(github, owner, repo, prNumber, labels, headSha, core) {
if (!labels.includes(CI_FIX_LABEL)) return false;
const ciGreen = await mergeGate.allChecksGreenOnSha(github, owner, repo, headSha, core);
if (!ciGreen) return false;
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: CI_FIX_LABEL,
});
core?.info?.(`Removed ${CI_FIX_LABEL} from PR #${prNumber}`);
} catch (err) {
if (err.status !== 404) throw err;
}
return true;
}
async function maybeTriggerCiFixClaude(github, owner, repo, prNumber, core, opts = {}) {
const baseRepo = `${owner}/${repo}`;
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
const pr = ctx.pr;
if (pr.state !== 'open') return { skipped: true, reason: 'not open' };
if (pr.draft) return { skipped: true, reason: 'draft' };
const headRepo = pr.head.repo?.full_name;
if (headRepo && headRepo !== baseRepo && !pr.maintainer_can_modify) {
return { skipped: true, reason: 'fork without maintainer edits' };
}
const headSha = pr.head.sha;
const labels = ctx.labels;
if (await maybeClearCiFixLabel(github, owner, repo, prNumber, labels, headSha, core)) {
return { skipped: true, reason: 'ci green, label cleared' };
}
const ciGreen = await mergeGate.allChecksGreenOnSha(github, owner, repo, headSha, core);
if (ciGreen) return { skipped: true, reason: 'ci green' };
const runs = await mergeGate.listChecksOnSha(github, owner, repo, headSha);
const failedRuns = mergeGate.listFailedChecksOnSha(runs);
if (failedRuns.length === 0) {
return { skipped: true, reason: 'no failed checks yet (may be pending)' };
}
const claudeInProgress = await mergeGate.hasInProgressClaudeAssistant(
github, owner, repo, prNumber
);
const skip = shouldSkipCiFix({
comments: ctx.comments,
headSha,
labels,
claudeInProgress,
failedChecks: failedRuns,
});
if (skip.skip) return { skipped: true, reason: skip.reason };
const failedChecks = mapFailedChecks(failedRuns);
const failureSummaries = [];
for (const check of failedChecks.slice(0, MAX_JOBS_TO_FETCH)) {
failureSummaries.push(
await fetchJobFailureSummary(github, owner, repo, check.id, check.name)
);
}
const body = buildCiFixComment({ headSha, failedChecks, failureSummaries });
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: [CI_FIX_LABEL],
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body,
});
core?.info?.(`Posted CI fix @claude on PR #${prNumber} (${shortSha(headSha)})`);
return { triggered: true, headSha: shortSha(headSha) };
}
async function processWorkflowRunFailure(github, owner, repo, workflowRun, core) {
if (workflowRun.conclusion !== 'failure') {
return { skipped: true, reason: `conclusion=${workflowRun.conclusion}` };
}
const allowed = config.ciFailureWorkflowRuns;
if (!allowed.includes(workflowRun.name)) {
return { skipped: true, reason: `workflow ${workflowRun.name}` };
}
const prNumber = await mergeGate.resolvePrNumberFromWorkflowRun(
github, owner, repo, workflowRun
);
if (!prNumber) return { skipped: true, reason: 'no linked PR' };
return maybeTriggerCiFixClaude(github, owner, repo, prNumber, core);
}
module.exports = {
CI_FIX_LABEL,
COOLDOWN_MS,
ciFixShaMarker,
isCiFixComment,
hasCiFixCommentForSha,
hasRecentCiFixComment,
shouldSkipCiFix,
parsePytestFailures,
fetchJobFailureSummary,
buildCiFixComment,
maybeClearCiFixLabel,
maybeTriggerCiFixClaude,
processWorkflowRunFailure,
};
+51
View File
@@ -0,0 +1,51 @@
/**
* PraisonAIUI gate configuration — imported by merge-gate, release-gate, ci-failure-claude.
*/
module.exports = {
repoFullName: 'MervinPraison/PraisonAIUI',
productPathPrefixes: ['src/praisonaiui/', 'src/frontend/', 'tests/'],
sensitivePathPatterns: [
/^\.github\/workflows\//,
/^pyproject\.toml$/,
/credentials\.json$/i,
],
requiredCheckPatterns: [/^ci$/i, /python/i, /test/i, /lint/i, /ruff/i],
ciWorkflowFile: 'ci.yml',
ciWorkflowName: 'CI',
mergeGateWorkflowRuns: ['CI', 'Claude Assistant'],
ciFailureWorkflowRuns: ['CI'],
pypiPackageName: 'aiui',
packagePaths: ['src/praisonaiui', 'src/frontend', 'pyproject.toml'],
finalClaudeScope:
'SCOPE: Focus ONLY on PraisonAIUI (src/praisonaiui, src/frontend, tests, docs). '
+ 'Do NOT expand into praisonaiagents or the monorepo unless the PR explicitly requires it.',
finalClaudeProductValue:
'4. Product value: review in depth whether the change genuinely adds PraisonAIUI UX value — '
+ 'never add features for the sake of adding them. It must strengthen the UI (simpler, '
+ 'more user-friendly, robust). If it does not clearly add value, request changes or recommend '
+ 'rejecting/closing rather than merging scope creep.\n'
+ '5. Layering: UI/frontend/server features here; core agent runtime belongs in praisonaiagents '
+ '(PraisonAI monorepo). Agent-callable tools → PraisonAI-Tools. Do not pull monorepo or SDK '
+ 'scope into this repo unless the PR explicitly requires it.\n'
+ '6. Do not bloat server.py or create_app() with new params — prefer existing YAML config, '
+ 'plugins, and feature modules; only add params if absolutely required. Reject knobs/exports '
+ 'with no live consumer.',
mergeGateProductValue:
'Confirm product value gate: change strengthens PraisonAIUI UX (no scope creep). The aim is a '
+ 'LIGHTWEIGHT AND POWERFUL package — BLOCK changes that add params/modules/exports duplicating '
+ 'existing UI capabilities (YAML config, plugins, feature modules, existing server hooks), or '
+ 'add knobs/fields with no live consumer, i.e. scope creep for the sake of it.',
mergeGateLayering:
'Layering routing: BLOCK if core agent logic was added here instead of praisonaiagents; BLOCK if '
+ 'agent-callable tools were added here instead of PraisonAI-Tools.',
mergeGateServerBloat:
'Do not bloat server.py or create_app() with additional params — only if absolutely required; '
+ 'prefer existing config/plugin patterns.',
agentPyChecks: false,
reviewBotLogins: [
'coderabbitai[bot]',
'qodo-code-review[bot]',
'greptile-apps[bot]',
],
};
+234
View File
@@ -0,0 +1,234 @@
#!/usr/bin/env node
/**
* Local self-test for merge-gate.js heuristics (no GitHub API).
* Run: node .github/scripts/merge-gate-selftest.js
*/
const mg = require('./merge-gate.js');
let failed = 0;
function assert(name, cond) {
if (!cond) {
console.error('FAIL:', name);
failed++;
} else {
console.log('ok:', name);
}
}
// Stale FINAL: push after FINAL, no @claude since head
const finals = [
{ user: { login: 'github-actions[bot]' }, body: '@claude FINAL architecture reviewer', created_at: '2026-06-12T08:00:00Z' },
];
assert('stale when head after final', mg.isStaleFinalAfterPush(finals, '2026-06-12T09:00:00Z'));
const withRecovery = [
...finals,
{ user: { login: 'github-actions[bot]' }, body: '@claude FINAL architecture reviewer', created_at: '2026-06-12T09:30:00Z' },
];
assert('not stale when @claude after head', !mg.isStaleFinalAfterPush(withRecovery, '2026-06-12T09:00:00Z'));
const withClaudeReply = [
...finals,
{
user: { login: 'praisonai-triage-agent[bot]' },
body: "**Claude finished @MervinPraison's task** —— [View job](https://github.com/)",
created_at: '2026-06-12T08:30:00Z',
},
];
assert('not stale when Claude replied after FINAL', !mg.isStaleFinalAfterPush(withClaudeReply, '2026-06-12T09:00:00Z'));
assert('claude final reply detected', mg.isClaudeFinalReplyComment(withClaudeReply[1]));
assert('cancelled detect-and-trigger does not block', mg.OPTIONAL_CANCELLED_CHECKS.has('detect-and-trigger'));
// Stale-FINAL recovery guards (PR #2560 push loop)
const nowMs = Date.now();
const iso = (ms) => new Date(ms).toISOString();
const firstFinal = {
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: iso(nowMs - 20 * 60 * 1000),
};
const pushSoonAfter = mg.shouldSkipStaleFinalRecovery(
[firstFinal],
iso(nowMs - 16 * 60 * 1000)
);
assert('debounce push soon after FINAL', pushSoonAfter.skip && pushSoonAfter.reason.includes('soon after'));
const secondFinal = {
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: iso(nowMs - 20 * 60 * 1000),
};
const capped = mg.shouldSkipStaleFinalRecovery(
[firstFinal, secondFinal],
iso(nowMs - 2 * 60 * 1000)
);
assert('hourly cap blocks third FINAL in window', capped.skip && capped.reason.includes('capped'));
const botPush = mg.shouldSkipStaleFinalRecovery(
[firstFinal],
iso(nowMs - 16 * 60 * 1000),
'praisonai-triage-agent[bot]'
);
assert('skip when automation pushed head', botPush.skip && botPush.reason.includes('automation'));
assert('claude automation login', mg.isClaudeAutomationLogin('praisonai-triage-agent[bot]'));
// Bot CHANGES_REQUESTED then APPROVE
const reviews = [
{ user: { login: 'coderabbit[bot]', type: 'Bot' }, state: 'CHANGES_REQUESTED', submitted_at: '2026-06-12T08:00:00Z' },
{ user: { login: 'coderabbit[bot]', type: 'Bot' }, state: 'APPROVED', submitted_at: '2026-06-12T09:00:00Z' },
];
assert('bot approve clears CR', !mg.hasAnyChangesRequested(reviews));
assert('human CR blocks', mg.hasAnyChangesRequested([
{ user: { login: 'MervinPraison', type: 'User' }, state: 'CHANGES_REQUESTED', submitted_at: '2026-06-12T09:00:00Z' },
]));
// Verdict after HEAD
const verdictComments = [
{ body: 'MERGE_GATE_VERDICT: APPROVE', created_at: '2026-06-12T08:00:00Z' },
];
assert('verdict before head rejected', mg.findMergeGateVerdict(verdictComments, null, '2026-06-12T09:00:00Z') === null);
assert('verdict after head accepted', mg.findMergeGateVerdict(
[{ body: 'MERGE_GATE_VERDICT: APPROVE', created_at: '2026-06-12T10:00:00Z' }],
null,
'2026-06-12T09:00:00Z'
) === 'APPROVE');
const fallbackApprove = [
{
body: 'MERGE_GATE_VERDICT: APPROVE\n\nAutomated fallback — Claude assess did not post a verdict comment.',
created_at: '2026-06-12T10:00:00Z',
},
];
assert(
'automated fallback APPROVE ignored when Opus required',
mg.findMergeGateVerdict(fallbackApprove, null, '2026-06-12T09:00:00Z', { excludeAutomatedFallback: true }) === null
);
assert(
'automated fallback APPROVE still visible without Opus-only filter',
mg.findMergeGateVerdict(fallbackApprove, null, '2026-06-12T09:00:00Z') === 'APPROVE'
);
const fallbackBlockNewFormat = [
{
body: 'MERGE_GATE_VERDICT: BLOCK\n\nAutomated fallback — Opus merge gate assessment did not complete.',
created_at: '2026-06-12T10:00:00Z',
},
];
assert(
'new-format fallback BLOCK ignored when Opus required',
mg.findMergeGateVerdict(fallbackBlockNewFormat, null, '2026-06-12T09:00:00Z', { excludeAutomatedFallback: true }) === null
);
assert(
'new-format fallback BLOCK detected by marker helper',
mg.isAutomatedFallbackVerdict(fallbackBlockNewFormat[0].body)
);
assert(
'findMergeGateVerdict null-safe with explicit null options',
mg.findMergeGateVerdict(fallbackApprove, null, '2026-06-12T09:00:00Z', null) === 'APPROVE'
);
const noise = [{ user: { login: 'MervinPraison' }, body: '**Merge gate scan** — wait for `@claude`', created_at: new Date().toISOString() }];
assert('diagnostic comment not a trigger', !mg.hasRecentClaudeTrigger(noise, 35));
// Cooldown skip requires an actual verdict on HEAD, not just a FINAL trigger comment
const recentFinalOnHead = [
{
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: '2026-06-27T10:00:00Z',
},
];
assert(
'final trigger alone does not count as verdict on head',
mg.findMergeGateVerdict(recentFinalOnHead, null, '2026-06-27T09:55:00Z') === null
);
const recentVerdictOnHead = [
...recentFinalOnHead,
{
user: { login: 'github-actions[bot]' },
body: 'MERGE_GATE_VERDICT: APPROVE',
created_at: '2026-06-27T10:05:00Z',
},
];
assert(
'verdict on head skips cooldown gate',
mg.findMergeGateVerdict(recentVerdictOnHead, null, '2026-06-27T09:55:00Z') === 'APPROVE'
);
// Sensitive + secrets
assert('workflow path sensitive', mg.sensitivePathReasons([{ filename: '.github/workflows/foo.yml' }]).length === 1);
assert('ci-only label exempts workflows', mg.sensitivePathReasons(
[{ filename: '.github/workflows/foo.yml' }],
[mg.WORKFLOW_ONLY_LABEL]
).length === 0);
assert('ci-only label not exempt mixed changes', mg.sensitivePathReasons(
[{ filename: '.github/workflows/foo.yml' }, { filename: 'src/praisonaiui/foo.py' }],
[mg.WORKFLOW_ONLY_LABEL]
).length === 1);
assert('secret in patch', mg.secretScanReasons([{ filename: 'x.py', patch: '+key = "ghp_abcdefghijklmnopqrstuvwxyz1234567890"' }]).length === 1);
// Claude run scoping — other PR branches must not block
assert('other branch claude does not block', !mg.hasBlockingClaudeRunForPr(
[{ event: 'issue_comment', head_branch: 'other-branch' }],
'my-branch'
));
assert('same branch claude blocks', mg.hasBlockingClaudeRunForPr(
[{ event: 'issue_comment', head_branch: 'my-branch' }],
'my-branch'
));
assert('issues event never blocks', !mg.hasBlockingClaudeRunForPr(
[{ event: 'issues', head_branch: 'my-branch' }],
'my-branch'
));
// Conflict rebase clears after bot completion + FINAL on HEAD (within 12h cooldown)
const conflictNowMs = Date.now();
const conflictIso = (offsetMs) => new Date(conflictNowMs + offsetMs).toISOString();
const conflictTrigger = {
user: { login: 'MervinPraison' },
body: '@claude this PR has merge conflicts with main. Please rebase',
created_at: conflictIso(-30 * 60 * 1000),
};
const rebaseDone = {
user: { login: 'praisonai-triage-agent[bot]' },
body: 'Rebase complete — PR #2308 onto latest main',
created_at: conflictIso(-29 * 60 * 1000),
};
const finalAfterRebase = {
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: conflictIso(-20 * 60 * 1000),
};
const headAfterRebase = conflictIso(-21 * 60 * 1000);
const rebaseComments = [conflictTrigger, rebaseDone, finalAfterRebase];
assert('conflict blocks before rebase done', mg.hasRecentConflictComment([conflictTrigger], headAfterRebase));
assert('conflict clears after rebase + FINAL on HEAD', !mg.hasRecentConflictComment(rebaseComments, headAfterRebase));
assert('conflict clears after rebase without FINAL on HEAD', !mg.hasRecentConflictComment(
[conflictTrigger, rebaseDone],
headAfterRebase
));
// Tests heuristic
assert('product code without tests', mg.missingTestsReason([{ filename: 'src/praisonaiui/a/b.py', additions: 3 }]) !== null);
assert('product code with tests ok', mg.missingTestsReason([
{ filename: 'src/praisonaiui/a/b.py', additions: 3 },
{ filename: 'tests/test_x.py', additions: 10 },
]) === null);
// PR size
assert('large PR blocked', mg.prSizeReasons([{ additions: 900 }]).length > 0);
assert('internal PR link accepted', mg.isInternalPullRequestLink(
{ base: { repo: { full_name: 'MervinPraison/PraisonAIUI' } } },
'MervinPraison',
'PraisonAIUI'
));
assert('fork sync PR link rejected', !mg.isInternalPullRequestLink(
{ number: 21, base: { repo: { full_name: 'Milkmange/PraisonAIUI' } } },
'MervinPraison',
'PraisonAIUI'
));
process.exit(failed ? 1 : 0);
+940
View File
@@ -0,0 +1,940 @@
/**
* Shared merge-gate helpers for claude-merge-gate.yml
* @see .github/workflows/claude-merge-gate.yml
*/
const config = require('./gate-config.js');
const CLAUDE_TRIGGER_LOGINS = ['MervinPraison', 'github-actions[bot]'];
const AUTO_ACTORS = CLAUDE_TRIGGER_LOGINS;
const CONFLICT_COOLDOWN_MS = 12 * 60 * 60 * 1000;
const CLAUDE_ACTIVE_MS = 35 * 60 * 1000;
const STALE_FINAL_RECOVERY_WINDOW_MS = 60 * 60 * 1000;
const STALE_FINAL_MAX_PER_WINDOW = 2;
const PUSH_AFTER_FINAL_DEBOUNCE_MS = 15 * 60 * 1000;
const ALLOWED_MERGE_STATES = new Set(['CLEAN', 'UNSTABLE']);
const AGENT_PY_MAX_AUTO_LINES = 100;
const PR_MAX_AUTO_ADDITIONS = 800;
const PR_MAX_AUTO_FILES = 30;
const MANUAL_ONLY_LABELS = new Set(['security', 'breaking-change', 'needs-manual-review', 'release']);
const WORKFLOW_ONLY_LABEL = 'merge-gate-ci-only';
const CI_ONLY_PATH_PREFIXES = ['.github/workflows/', '.github/actions/', '.github/scripts/merge-gate'];
const SDK_PATH_PREFIXES = config.productPathPrefixes;
const SENSITIVE_PATH_PATTERNS = config.sensitivePathPatterns;
const REQUIRED_SDK_CHECK_PATTERNS = config.requiredCheckPatterns;
const SECRET_PATTERNS = [
/sk-[a-zA-Z0-9]{20,}/,
/AKIA[0-9A-Z]{16}/,
/ghp_[a-zA-Z0-9]{36,}/,
/github_pat_/,
/Bearer eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/,
/-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/,
];
const BLOCK_LABELS = new Set([
'claude-conflict-pending',
'claude-merge-gate-active',
'no-auto-merge',
'auto-merged-by-gate',
]);
const MERGE_READY_LABEL = 'pipeline/merge-ready';
/** Superseded concurrency runs; must not block merge when real tests passed. */
const OPTIONAL_CANCELLED_CHECKS = new Set(['detect-and-trigger']);
const BOT_REVIEWER_PATTERNS = [
'coderabbit',
'qodo',
'gemini',
'copilot',
'greptile',
];
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function isFinalClaudeTriggerComment(c) {
const body = (c.body || '').toLowerCase();
if (!AUTO_ACTORS.includes(c.user.login)) return false;
if (!body.includes('@claude')) return false;
if (body.includes('merge conflict')) return false;
return body.includes('final architecture reviewer') || body.includes('lead engineer');
}
function isClaudeTriggerNoise(c) {
const body = c.body || '';
if (body.includes('Merge gate scan')) return true;
if (body.includes('MERGE_GATE_VERDICT')) return true;
if (body.includes('Merged by **Claude PR merge gate**')) return true;
return false;
}
function isClaudeFinalReplyComment(c) {
const login = (c.user?.login || '').toLowerCase();
const fromAutomation =
login.includes('praisonai-triage') ||
login.includes('github-actions') ||
AUTO_ACTORS.some((a) => a.toLowerCase() === login);
if (!fromAutomation) return false;
return (c.body || '').includes('Claude finished');
}
function hasRecentClaudeTrigger(comments, minutes = 35) {
const cutoff = Date.now() - minutes * 60 * 1000;
return comments.some((c) => {
if (!CLAUDE_TRIGGER_LOGINS.includes(c.user.login)) return false;
if (isClaudeTriggerNoise(c)) return false;
if (!(c.body || '').includes('@claude')) return false;
return new Date(c.created_at).getTime() > cutoff;
});
}
function isConflictRebaseTriggerComment(c) {
if (!AUTO_ACTORS.includes(c.user.login)) return false;
const body = (c.body || '').toLowerCase();
return body.includes('@claude') && body.includes('merge conflict');
}
function isConflictRebaseCompletionComment(c) {
const login = (c.user?.login || '').toLowerCase();
const fromAutomation =
login.includes('praisonai-triage') ||
login.includes('github-actions') ||
AUTO_ACTORS.some((a) => a.toLowerCase() === login);
if (!fromAutomation) return false;
const body = (c.body || '').toLowerCase();
return (
body.includes('rebase complete') ||
body.includes('rebase onto') ||
body.includes('force-pushed') ||
(body.includes('conflict') && body.includes('resolved'))
);
}
/** HEAD landed after conflict rebase but no FINAL @claude since that push. */
function needsFinalReReviewAfterConflictRebase(comments, headPushedAt) {
if (!headPushedAt) return false;
const headTime = new Date(headPushedAt).getTime();
const conflictTriggers = comments.filter(isConflictRebaseTriggerComment);
if (conflictTriggers.length === 0) return false;
const latestConflict = conflictTriggers.reduce((a, b) =>
new Date(a.created_at) > new Date(b.created_at) ? a : b
);
const conflictTime = new Date(latestConflict.created_at).getTime();
if (headTime <= conflictTime) return false;
const rebaseDone = comments.some(
(c) =>
new Date(c.created_at).getTime() > conflictTime && isConflictRebaseCompletionComment(c)
);
if (!rebaseDone) return false;
const finalsAfterHead = comments.filter(
(c) =>
isFinalClaudeTriggerComment(c) &&
new Date(c.created_at).getTime() >= headTime - 60000
);
return finalsAfterHead.length === 0;
}
function conflictRebaseQuiescent(comments, headPushedAt) {
const conflictTriggers = comments.filter(isConflictRebaseTriggerComment);
if (conflictTriggers.length === 0) return true;
const latestConflict = conflictTriggers.reduce((a, b) =>
new Date(a.created_at) > new Date(b.created_at) ? a : b
);
const conflictTime = new Date(latestConflict.created_at).getTime();
const rebaseDone = comments.some(
(c) =>
new Date(c.created_at).getTime() > conflictTime && isConflictRebaseCompletionComment(c)
);
if (!rebaseDone) return false;
return true;
}
function hasRecentConflictComment(comments, headPushedAt = null) {
const cutoff = Date.now() - CONFLICT_COOLDOWN_MS;
const hasRecentTrigger = comments.some((c) => {
if (!isConflictRebaseTriggerComment(c)) return false;
return new Date(c.created_at).getTime() > cutoff;
});
if (!hasRecentTrigger) return false;
if (headPushedAt && conflictRebaseQuiescent(comments, headPushedAt)) {
return false;
}
return true;
}
function isBotReviewer(login, userType) {
const lower = (login || '').toLowerCase();
if (userType === 'Bot') return true;
if (lower.endsWith('[bot]')) return true;
return ['coderabbit', 'qodo', 'gemini', 'copilot', 'greptile'].some((p) => lower.includes(p));
}
function latestReviewsByUser(reviews) {
const latestByUser = new Map();
for (const r of reviews) {
const login = r.user?.login;
if (!login) continue;
const prev = latestByUser.get(login);
if (!prev || new Date(r.submitted_at) > new Date(prev.submitted_at)) {
latestByUser.set(login, r);
}
}
return latestByUser;
}
function hasHumanChangesRequested(reviews) {
for (const [login, review] of latestReviewsByUser(reviews)) {
if (review.state !== 'CHANGES_REQUESTED') continue;
if (!isBotReviewer(login, review.user?.type)) return true;
}
return false;
}
function hasAnyChangesRequested(reviews) {
for (const [, review] of latestReviewsByUser(reviews)) {
if (review.state === 'CHANGES_REQUESTED') return true;
}
return false;
}
function hasFinalClaudeReviewTrigger(comments) {
return comments.some(isFinalClaudeTriggerComment);
}
function isStaleFinalAfterPush(comments, headPushedAt) {
if (!headPushedAt) return false;
const headTime = new Date(headPushedAt).getTime();
const finals = comments.filter(isFinalClaudeTriggerComment);
if (finals.length === 0) return true;
const latestFinal = finals.reduce((a, b) =>
new Date(a.created_at) > new Date(b.created_at) ? a : b
);
const finalTime = new Date(latestFinal.created_at).getTime();
if (headTime <= finalTime + 60000) return false;
const claudeRepliedAfterFinal = comments.some((c) => {
if (!isClaudeFinalReplyComment(c)) return false;
return new Date(c.created_at).getTime() >= finalTime - 60000;
});
if (claudeRepliedAfterFinal) return false;
const claudeSinceHead = comments.some((c) => {
if (!CLAUDE_TRIGGER_LOGINS.includes(c.user.login)) return false;
if (isClaudeTriggerNoise(c)) return false;
if (!(c.body || '').includes('@claude')) return false;
return new Date(c.created_at).getTime() >= headTime - 60000;
});
return !claudeSinceHead;
}
function needsStaleFinalRecovery(comments, headPushedAt) {
return (
hasFinalClaudeReviewTrigger(comments) &&
isStaleFinalAfterPush(comments, headPushedAt)
);
}
function shouldSkipFinalRecovery(comments, headPushedAt) {
const isStale = isStaleFinalAfterPush(comments, headPushedAt);
if (isStale) return false;
return hasRecentClaudeTrigger(comments, 35);
}
function countFinalTriggersSince(comments, sinceMs) {
return comments.filter(
(c) => isFinalClaudeTriggerComment(c) && new Date(c.created_at).getTime() > sinceMs
).length;
}
function isClaudeAutomationLogin(login) {
const lower = (login || '').toLowerCase();
return lower.includes('praisonai-triage') || lower === 'github-actions[bot]';
}
function isPushSoonAfterLatestFinal(comments, headPushedAt) {
if (!headPushedAt) return false;
const finals = comments.filter(isFinalClaudeTriggerComment);
if (finals.length === 0) return false;
const latestFinal = finals.reduce((a, b) =>
new Date(a.created_at) > new Date(b.created_at) ? a : b
);
const finalTime = new Date(latestFinal.created_at).getTime();
const headTime = new Date(headPushedAt).getTime();
if (headTime <= finalTime) return false;
return headTime - finalTime < PUSH_AFTER_FINAL_DEBOUNCE_MS;
}
/** Returns { skip: true, reason } when stale-FINAL recovery should not post. */
function shouldSkipStaleFinalRecovery(comments, headPushedAt, headPusherLogin = null) {
if (!isStaleFinalAfterPush(comments, headPushedAt)) {
return { skip: true, reason: 'not stale' };
}
if (headPusherLogin && isClaudeAutomationLogin(headPusherLogin)) {
return { skip: true, reason: 'head pushed by Claude automation' };
}
if (needsFinalReReviewAfterConflictRebase(comments, headPushedAt)) {
return { skip: false, reason: '' };
}
if (isPushSoonAfterLatestFinal(comments, headPushedAt)) {
return {
skip: true,
reason: 'head pushed soon after FINAL (wait for CI / batched fixes)',
};
}
const windowStart = Date.now() - STALE_FINAL_RECOVERY_WINDOW_MS;
const finalsInWindow = countFinalTriggersSince(comments, windowStart);
if (finalsInWindow >= STALE_FINAL_MAX_PER_WINDOW) {
return {
skip: true,
reason: `stale-FINAL capped (${finalsInWindow} FINAL triggers in last hour)`,
};
}
return { skip: false, reason: '' };
}
function finalClaudeCompletedOnSha(comments, headPushedAt) {
if (!hasFinalClaudeReviewTrigger(comments)) return false;
if (isStaleFinalAfterPush(comments, headPushedAt)) return false;
return true;
}
const FINAL_CLAUDE_REVIEW_BODY =
`@claude You are the FINAL architecture reviewer. If the branch is under ${config.repoFullName} (not a fork), you are able to make modifications to this branch and push directly. ${config.finalClaudeScope} Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions (lazy imports, import-time <200ms)\n${config.finalClaudeProductValue}\n\n**Phase 2: FIX Valid Issues**\n5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n6. Push all code fixes directly to THIS branch (do NOT create a new PR)\n7. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n8. If all issues are resolved, approve the PR / close the Issue\n9. If blocking issues remain, request changes / leave clear action items`;
async function getMergeState(github, owner, repo, prNumber) {
const query = `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
mergeStateStatus
maintainerCanModify
isDraft
headRefOid
headRef { repository { nameWithOwner } }
}
}
}
`;
for (let attempt = 0; attempt < 3; attempt++) {
const result = await github.graphql(query, { owner, repo, number: prNumber });
const prGql = result.repository.pullRequest;
const status = (prGql?.mergeStateStatus || '').toUpperCase();
if (status && status !== 'UNKNOWN') {
return {
status,
isDraft: prGql.isDraft,
headRepo: prGql.headRef?.repository?.nameWithOwner,
headSha: prGql.headRefOid,
maintainerCanModify: prGql.maintainerCanModify === true,
};
}
if (attempt < 2) await sleep(10000);
}
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
return {
status: (pr.mergeable_state || '').toUpperCase(),
isDraft: pr.draft,
headRepo: pr.head.repo?.full_name,
headSha: pr.head.sha,
maintainerCanModify: pr.maintainer_can_modify === true,
};
}
async function listChecksOnSha(github, owner, repo, sha) {
const { data } = await github.rest.checks.listForRef({
owner,
repo,
ref: sha,
per_page: 100,
});
return (data.check_runs || []).filter((r) => r.head_sha === sha);
}
function listFailedChecksOnSha(runs) {
return (runs || []).filter((run) => {
if (run.status !== 'completed') return false;
return run.conclusion === 'failure';
});
}
async function allChecksGreenOnSha(github, owner, repo, sha, core) {
const runs = await listChecksOnSha(github, owner, repo, sha);
if (runs.length === 0) {
core?.info?.(`No check runs on ${sha.slice(0, 7)} — allowing (e.g. docs-only PR)`);
return true;
}
for (const run of runs) {
if (run.status !== 'completed') {
core?.info?.(`Check pending: ${run.name} (${run.status})`);
return false;
}
const ok =
['success', 'neutral', 'skipped'].includes(run.conclusion) ||
(run.conclusion === 'cancelled' && OPTIONAL_CANCELLED_CHECKS.has(run.name));
if (!ok) {
core?.info?.(`Check failed: ${run.name} (${run.conclusion})`);
return false;
}
}
return true;
}
function claudeRunBlocksPr(run, headRef) {
if (!run || run.event === 'issues') return false;
if (!headRef) return true;
return run.head_branch === headRef;
}
function hasBlockingClaudeRunForPr(runs, headRef) {
return (runs || []).some((r) => claudeRunBlocksPr(r, headRef));
}
async function hasInProgressClaudeAssistant(github, owner, repo, prNumber = null) {
try {
const { data } = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'claude.yml',
status: 'in_progress',
per_page: 20,
});
const runs = (data.workflow_runs || []).filter((r) => r.event !== 'issues');
if (runs.length === 0) return false;
if (prNumber == null) return runs.length > 0;
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
const headRef = pr.head.ref;
return hasBlockingClaudeRunForPr(runs, headRef);
} catch {
return false;
}
}
function isCiOnlyChange(files) {
if (!files.length) return false;
return files.every((f) => CI_ONLY_PATH_PREFIXES.some((p) => f.filename.startsWith(p)));
}
function isInternalPullRequestLink(link, owner, repo) {
if (link?.base?.repo?.full_name === `${owner}/${repo}`) return true;
const baseUrl = link?.base?.repo?.url || '';
return baseUrl.endsWith(`/repos/${owner}/${repo}`);
}
async function resolvePrNumberFromLinkedPullRequests(github, owner, repo, linked) {
const repoFull = `${owner}/${repo}`;
const internal = (linked || []).filter((l) => isInternalPullRequestLink(l, owner, repo));
for (const link of internal) {
if (!link.number) continue;
try {
const { data } = await github.rest.pulls.get({
owner,
repo,
pull_number: link.number,
});
if (data.state === 'open' && data.base?.repo?.full_name === repoFull) {
return data.number;
}
} catch (err) {
if (err.status !== 404) throw err;
}
}
return null;
}
async function resolvePrNumberFromHeadBranch(github, owner, repo, branch) {
if (!branch || branch === 'main' || branch === 'master') return null;
const { data } = await github.rest.pulls.list({
owner,
repo,
state: 'open',
head: `${owner}:${branch}`,
per_page: 1,
});
return data[0]?.number || null;
}
async function resolvePrNumberFromWorkflowRun(github, owner, repo, workflowRun) {
const fromLinked = await resolvePrNumberFromLinkedPullRequests(
github, owner, repo, workflowRun.pull_requests
);
if (fromLinked) return fromLinked;
const fromBranch = await resolvePrNumberFromHeadBranch(
github, owner, repo, workflowRun.head_branch
);
if (fromBranch) return fromBranch;
return null;
}
async function listPullFiles(github, owner, repo, prNumber) {
if (typeof github.paginate === 'function') {
return github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
}
const { data } = await github.rest.pulls.listFiles({
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
return data;
}
function countNewAgentParams(patch) {
if (!patch) return 0;
return patch
.split('\n')
.filter((l) => l.startsWith('+') && !l.startsWith('+++'))
.filter((l) => {
const t = l.slice(1).trim();
if (!t || t.startsWith('#') || t.startsWith('"""') || t.startsWith("'''")) return false;
if (/^(def|class|@|return\b|if\b|elif\b|else\b|for\b|while\b)/.test(t)) return false;
if (/^\w+\s*[:=]/.test(t) && !t.startsWith('self.')) return true;
return false;
}).length;
}
function getAgentPyChangeFromFiles(files) {
const agentFile = files.find((f) => f.filename.endsWith('praisonaiagents/agent/agent.py'));
if (!agentFile) {
return { touched: false, additions: 0, newParams: 0 };
}
return {
touched: true,
additions: agentFile.additions || 0,
newParams: countNewAgentParams(agentFile.patch || ''),
};
}
async function getAgentPyChange(github, owner, repo, prNumber) {
const files = await listPullFiles(github, owner, repo, prNumber);
return getAgentPyChangeFromFiles(files);
}
function touchesSdk(files) {
return files.some((f) => SDK_PATH_PREFIXES.some((p) => f.filename.startsWith(p)));
}
function hasManualOnlyLabel(labels) {
return labels.some((l) => MANUAL_ONLY_LABELS.has(l));
}
function sensitivePathReasons(files, labels = []) {
if (labels.includes(WORKFLOW_ONLY_LABEL) && isCiOnlyChange(files)) {
return [];
}
const reasons = [];
for (const f of files) {
if (SENSITIVE_PATH_PATTERNS.some((p) => p.test(f.filename))) {
reasons.push(`sensitive path: ${f.filename}`);
break;
}
}
return reasons;
}
function prSizeReasons(files) {
const reasons = [];
const totalAdditions = files.reduce((sum, f) => sum + (f.additions || 0), 0);
if (totalAdditions > PR_MAX_AUTO_ADDITIONS) {
reasons.push(`PR +${totalAdditions} lines (>${PR_MAX_AUTO_ADDITIONS}) requires manual review`);
}
if (files.length > PR_MAX_AUTO_FILES) {
reasons.push(`${files.length} files changed (>${PR_MAX_AUTO_FILES}) requires manual review`);
}
return reasons;
}
function missingTestsReason(files) {
const productAdds = files.filter(
(f) =>
SDK_PATH_PREFIXES.some((p) => f.filename.startsWith(p)) &&
f.filename.startsWith('src/praisonaiui/') &&
f.filename.endsWith('.py') &&
!f.filename.endsWith('__init__.py') &&
(f.additions || 0) > 0
);
if (productAdds.length === 0) return null;
const hasTestChange = files.some(
(f) => /\/tests?\//.test(f.filename) || /test_.*\.py$/.test(f.filename) || /_test\.py$/.test(f.filename)
);
if (!hasTestChange) return 'Product code added without test file changes — requires manual review';
return null;
}
function secretScanReasons(files) {
for (const f of files) {
if (/\/tests?\//.test(f.filename) || /test_.*\.py$/.test(f.filename)) continue;
const patch = f.patch || '';
if (!patch) continue;
for (const pattern of SECRET_PATTERNS) {
if (pattern.test(patch)) {
return [`possible secret in diff (${f.filename}) — requires manual review`];
}
}
}
return [];
}
async function sdkTestChecksReason(github, owner, repo, sha, files, core) {
if (!touchesSdk(files)) return null;
const { data } = await github.rest.checks.listForRef({
owner,
repo,
ref: sha,
per_page: 100,
});
const runs = (data.check_runs || []).filter((r) => r.head_sha === sha);
if (runs.length === 0) {
return 'SDK code changed but no CI checks on HEAD';
}
const testRuns = runs.filter((r) => REQUIRED_SDK_CHECK_PATTERNS.some((p) => p.test(r.name || '')));
if (testRuns.length === 0) {
return 'SDK code changed but no test check runs on HEAD';
}
core?.info?.(`SDK test checks on HEAD: ${testRuns.map((r) => r.name).join(', ')}`);
return null;
}
function manualReviewReasonForAgentPy(agentChange) {
if (!agentChange.touched) return null;
if (agentChange.additions > AGENT_PY_MAX_AUTO_LINES) {
return `agent.py +${agentChange.additions} lines (>${AGENT_PY_MAX_AUTO_LINES}) requires manual review`;
}
if (agentChange.newParams > 0) {
return `agent.py adds ${agentChange.newParams} new Agent param(s) — requires manual review`;
}
return null;
}
async function listAllComments(github, owner, repo, issueNumber) {
if (typeof github.paginate === 'function') {
return github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
}
const { data } = await github.rest.issues.listComments({
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
return data;
}
async function getHeadCommitDate(github, owner, repo, prNumber) {
try {
let commits;
if (typeof github.paginate === 'function') {
commits = await github.paginate(github.rest.pulls.listCommits, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
} else {
const { data } = await github.rest.pulls.listCommits({
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
commits = data;
}
const last = commits[commits.length - 1];
return last?.commit?.committer?.date || last?.commit?.author?.date || null;
} catch {
return null;
}
}
async function loadPrContext(github, owner, repo, prNumber) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: prNumber });
const comments = await listAllComments(github, owner, repo, prNumber);
const { data: reviews } = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
const mergeState = await getMergeState(github, owner, repo, prNumber);
const headSha = mergeState.headSha || pr.head.sha;
const headCommitDate = await getHeadCommitDate(github, owner, repo, prNumber);
const headPushedAt = headCommitDate || pr.updated_at;
return {
pr,
issue,
comments,
reviews,
mergeState,
headSha,
headPushedAt,
labels: issue.labels.map((l) => l.name),
baseRepo: `${owner}/${repo}`,
};
}
async function evaluatePipelineQuiescent(github, owner, repo, prNumber, core, options = {}) {
const {
forMergeStep = false,
skipGlobalClaudeRunCheck = false,
skipRecentClaudeCooldown = false,
} = options;
const ctx = await loadPrContext(github, owner, repo, prNumber);
const reasons = [];
if (ctx.pr.draft) reasons.push('draft');
if (ctx.pr.state !== 'open') reasons.push('not open');
if (ctx.labels.includes('auto-merged-by-gate')) reasons.push('already merged by gate');
if (ctx.labels.includes('no-auto-merge')) reasons.push('no-auto-merge label');
if (ctx.labels.includes('claude-conflict-pending')) reasons.push('claude-conflict-pending');
if (!forMergeStep && ctx.labels.includes('claude-merge-gate-active')) {
reasons.push('claude-merge-gate-active');
}
const { status, headRepo, maintainerCanModify } = ctx.mergeState;
if (!ALLOWED_MERGE_STATES.has(status)) reasons.push(`mergeState=${status}`);
if (headRepo && headRepo !== ctx.baseRepo) {
reasons.push('fork PR');
}
if (hasRecentConflictComment(ctx.comments, ctx.headPushedAt)) {
reasons.push('recent merge-conflict @claude');
}
if (!skipRecentClaudeCooldown && hasRecentClaudeTrigger(ctx.comments, 35)) {
const verdictOnHead = findMergeGateVerdict(ctx.comments, null, ctx.headPushedAt) !== null;
if (!verdictOnHead) reasons.push('recent @claude within 35min');
}
if (!skipGlobalClaudeRunCheck && (await hasInProgressClaudeAssistant(github, owner, repo, prNumber))) {
reasons.push('claude.yml in progress');
}
const checksOk = await allChecksGreenOnSha(github, owner, repo, ctx.headSha, core);
if (!checksOk) reasons.push('CI not green on HEAD');
if (!finalClaudeCompletedOnSha(ctx.comments, ctx.headPushedAt)) {
if (!hasFinalClaudeReviewTrigger(ctx.comments)) {
reasons.push('no FINAL Claude review trigger');
} else if (isStaleFinalAfterPush(ctx.comments, ctx.headPushedAt)) {
reasons.push('stale FINAL after new commits (needs @claude re-review)');
} else {
reasons.push('FINAL Claude not complete on HEAD');
}
}
if (hasAnyChangesRequested(ctx.reviews)) reasons.push('CHANGES_REQUESTED review');
if (hasManualOnlyLabel(ctx.labels)) {
const manualLabel = ctx.labels.find((l) => MANUAL_ONLY_LABELS.has(l));
reasons.push(`manual-only label: ${manualLabel}`);
}
const pullFiles = await listPullFiles(github, owner, repo, prNumber);
if (config.agentPyChecks) {
const agentChange = getAgentPyChangeFromFiles(pullFiles);
const agentManual = manualReviewReasonForAgentPy(agentChange);
if (agentManual) reasons.push(agentManual);
}
reasons.push(...sensitivePathReasons(pullFiles, ctx.labels));
reasons.push(...prSizeReasons(pullFiles));
reasons.push(...secretScanReasons(pullFiles));
const testsReason = missingTestsReason(pullFiles);
if (testsReason) reasons.push(testsReason);
const sdkCheckReason = await sdkTestChecksReason(github, owner, repo, ctx.headSha, pullFiles, core);
if (sdkCheckReason) reasons.push(sdkCheckReason);
if (ctx.pr.mergeable === false) reasons.push('not mergeable');
const ready = reasons.length === 0;
return {
ready,
reasons,
headSha: ctx.headSha,
prNumber,
};
}
async function listPrNumbersForMergeGateScan(github, owner, repo, core) {
let mergeReadyIssues;
if (typeof github.paginate === 'function') {
mergeReadyIssues = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: 'open',
labels: MERGE_READY_LABEL,
per_page: 100,
});
} else {
({ data: mergeReadyIssues } = await github.rest.issues.listForRepo({
owner,
repo,
state: 'open',
labels: MERGE_READY_LABEL,
per_page: 100,
}));
}
const mergeReady = mergeReadyIssues
.filter((issue) => issue.pull_request)
.map((issue) => issue.number)
.sort((a, b) => a - b);
if (mergeReady.length > 0) {
core?.info?.(
`Merge gate scan: ${mergeReady.length} ${MERGE_READY_LABEL} PR(s) (skipping unlabelled open PRs)`
);
return mergeReady;
}
core?.info?.(`No ${MERGE_READY_LABEL} PRs — scanning oldest open PRs`);
const prs =
typeof github.paginate === 'function'
? await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
per_page: 100,
sort: 'created',
direction: 'asc',
})
: (await github.rest.pulls.list({
owner,
repo,
state: 'open',
per_page: 100,
sort: 'created',
direction: 'asc',
})).data;
return prs.map((pr) => pr.number);
}
async function selectMergeGateCandidates(github, owner, repo, prNumbers, maxCandidates, core) {
const readyList = [];
const skipped = [];
for (const num of prNumbers) {
if (readyList.length >= maxCandidates) {
core?.info?.(`Found ${maxCandidates} candidate(s) — skipping remaining PR scans`);
break;
}
const result = await evaluatePipelineQuiescent(github, owner, repo, num, core);
if (result.ready) {
readyList.push({ pr_number: num, head_sha: result.headSha });
} else {
skipped.push({ pr: num, reasons: result.reasons });
}
}
readyList.sort((a, b) => a.pr_number - b.pr_number);
return {
candidates: readyList.slice(0, maxCandidates),
skipped,
};
}
const AUTOMATED_FALLBACK_MARKER = 'Automated fallback —';
function isAutomatedFallbackVerdict(body) {
return (body || '').includes(AUTOMATED_FALLBACK_MARKER);
}
function findMergeGateVerdict(comments, minCreatedAt = null, headPushedAt = null, options = {}) {
const { excludeAutomatedFallback = false } = options || {};
const minTime = minCreatedAt ? new Date(minCreatedAt).getTime() : 0;
const headTime = headPushedAt ? new Date(headPushedAt).getTime() - 60000 : 0;
const gateComments = comments
.filter((c) => {
if (!(c.body || '').includes('MERGE_GATE_VERDICT:')) return false;
if (excludeAutomatedFallback && isAutomatedFallbackVerdict(c.body)) return false;
const created = new Date(c.created_at).getTime();
if (minTime && created < minTime) return false;
if (headTime && created < headTime) return false;
return true;
})
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
if (gateComments.length === 0) return null;
const body = gateComments[0].body || '';
if (body.includes('MERGE_GATE_VERDICT: APPROVE')) return 'APPROVE';
if (body.includes('MERGE_GATE_VERDICT: BLOCK')) return 'BLOCK';
return null;
}
module.exports = {
CLAUDE_TRIGGER_LOGINS,
AUTO_ACTORS,
isFinalClaudeTriggerComment,
isClaudeTriggerNoise,
isClaudeFinalReplyComment,
hasRecentClaudeTrigger,
hasRecentConflictComment,
isConflictRebaseTriggerComment,
isConflictRebaseCompletionComment,
needsFinalReReviewAfterConflictRebase,
conflictRebaseQuiescent,
hasHumanChangesRequested,
hasAnyChangesRequested,
hasFinalClaudeReviewTrigger,
finalClaudeCompletedOnSha,
getMergeState,
OPTIONAL_CANCELLED_CHECKS,
listChecksOnSha,
listFailedChecksOnSha,
allChecksGreenOnSha,
hasInProgressClaudeAssistant,
claudeRunBlocksPr,
hasBlockingClaudeRunForPr,
isCiOnlyChange,
isInternalPullRequestLink,
resolvePrNumberFromLinkedPullRequests,
resolvePrNumberFromHeadBranch,
resolvePrNumberFromWorkflowRun,
WORKFLOW_ONLY_LABEL,
getAgentPyChange,
getAgentPyChangeFromFiles,
manualReviewReasonForAgentPy,
countNewAgentParams,
listPullFiles,
touchesSdk,
hasManualOnlyLabel,
sensitivePathReasons,
prSizeReasons,
missingTestsReason,
secretScanReasons,
sdkTestChecksReason,
listAllComments,
getHeadCommitDate,
isStaleFinalAfterPush,
needsStaleFinalRecovery,
shouldSkipFinalRecovery,
shouldSkipStaleFinalRecovery,
isClaudeAutomationLogin,
isPushSoonAfterLatestFinal,
countFinalTriggersSince,
STALE_FINAL_RECOVERY_WINDOW_MS,
STALE_FINAL_MAX_PER_WINDOW,
FINAL_CLAUDE_REVIEW_BODY,
loadPrContext,
evaluatePipelineQuiescent,
MERGE_READY_LABEL,
listPrNumbersForMergeGateScan,
selectMergeGateCandidates,
isAutomatedFallbackVerdict,
AUTOMATED_FALLBACK_MARKER,
findMergeGateVerdict,
};
@@ -0,0 +1,82 @@
#!/usr/bin/env node
/**
* Run: node .github/scripts/pipeline-status-selftest.js
*/
const ps = require('./pipeline-status.js');
const mg = require('./merge-gate.js');
const config = require('./gate-config.js');
let failed = 0;
function assert(name, cond) {
if (!cond) {
console.error('FAIL:', name);
failed++;
} else {
console.log('ok:', name);
}
}
const kickComments = [
{ user: { login: 'MervinPraison' }, body: '@coderabbitai review' },
{ user: { login: 'MervinPraison' }, body: '/review' },
];
const finalComment = {
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: '2026-06-26T10:00:00Z',
};
assert(
'reviews pending when not kicked',
ps.deriveStage([], { ready: false, reasons: ['no FINAL Claude review trigger'] })
=== 'pipeline/reviews-pending'
);
assert(
'final pending after kick',
ps.deriveStage(kickComments, { ready: false, reasons: ['no FINAL Claude review trigger'] })
=== 'pipeline/final-claude-pending'
);
assert(
'awaiting merge gate after bot review comment',
ps.deriveStage(
[
{ user: { login: 'coderabbitai[bot]' }, body: 'review summary' },
finalComment,
],
{ ready: false, reasons: ['recent @claude within 35min'] }
) === 'pipeline/awaiting-merge-gate'
);
assert('review bot logins configured', config.reviewBotLogins.includes('coderabbitai[bot]'));
assert(
'merge ready',
ps.deriveStage([...kickComments, finalComment], { ready: true, reasons: [] })
=== 'pipeline/merge-ready'
);
const blockers = ps.deriveBlockerLabels({
ready: false,
reasons: ['CI not green on HEAD', 'SDK code added without test file changes — requires manual review'],
});
assert('maps ci blocker', blockers.includes('pipeline/blocked:ci'));
assert('maps manual blocker', blockers.includes('pipeline/blocked:manual-review'));
assert(
'internal link matches upstream base',
mg.isInternalPullRequestLink(
{ base: { repo: { full_name: 'MervinPraison/PraisonAI' } } },
'MervinPraison',
'PraisonAI'
)
);
assert(
'fork sync link rejected',
!mg.isInternalPullRequestLink(
{ number: 21, base: { repo: { full_name: 'Milkmange/PraisonAI' } } },
'MervinPraison',
'PraisonAI'
)
);
process.exit(failed ? 1 : 0);
+264
View File
@@ -0,0 +1,264 @@
/**
* Sync pipeline stage/blocker labels on open PRs.
* @see .github/workflows/pipeline-status-sync.yml
*/
const mergeGate = require('./merge-gate.js');
const chain = require('./bot-pr-review-chain.js');
const ciFix = require('./ci-failure-claude.js');
const config = require('./gate-config.js');
const PIPELINE_PREFIX = 'pipeline/';
const STAGE_LABELS = [
'pipeline/reviews-pending',
'pipeline/final-claude-pending',
'pipeline/awaiting-merge-gate',
'pipeline/merge-ready',
'pipeline/merged',
];
const BLOCKER_LABELS = [
'pipeline/blocked:ci',
'pipeline/blocked:conflict',
'pipeline/blocked:manual-review',
'pipeline/blocked:cooldown',
'pipeline/blocked:stale-final',
'pipeline/blocked:no-final',
];
const ALL_PIPELINE_LABELS = [...STAGE_LABELS, ...BLOCKER_LABELS];
const LABEL_SPECS = [
{ name: 'pipeline/reviews-pending', color: 'fbca04', description: 'Waiting for CodeRabbit/Qodo/Copilot reviews' },
{ name: 'pipeline/final-claude-pending', color: 'd4c5f9', description: 'Reviews done; waiting for FINAL @claude' },
{ name: 'pipeline/awaiting-merge-gate', color: 'c5def5', description: 'FINAL done; waiting for merge gate / CI' },
{ name: 'pipeline/merge-ready', color: '0e8a16', description: 'Eligible for merge gate auto-merge' },
{ name: 'pipeline/merged', color: '6f42c1', description: 'Merged via pipeline' },
{ name: 'pipeline/blocked:ci', color: 'd93f0b', description: 'Blocked: CI not green on HEAD' },
{ name: 'pipeline/blocked:conflict', color: 'b60205', description: 'Blocked: merge conflict or rebase pending' },
{ name: 'pipeline/blocked:manual-review', color: 'e99695', description: 'Blocked: requires manual review' },
{ name: 'pipeline/blocked:cooldown', color: 'fef2c0', description: 'Blocked: post-push or @claude cooldown' },
{ name: 'pipeline/blocked:stale-final', color: 'f9d0c4', description: 'Blocked: FINAL stale after new commits' },
{ name: 'pipeline/blocked:no-final', color: 'ededed', description: 'Blocked: no FINAL @claude trigger yet' },
];
function reviewChainStarted(comments) {
if (chain.chainKickPosted(comments)) return true;
return comments.some((c) => config.reviewBotLogins.includes(c.user?.login));
}
function deriveStage(comments, evalResult) {
if (evalResult.ready) return 'pipeline/merge-ready';
if (!reviewChainStarted(comments)) return 'pipeline/reviews-pending';
if (!mergeGate.hasFinalClaudeReviewTrigger(comments)) return 'pipeline/final-claude-pending';
return 'pipeline/awaiting-merge-gate';
}
function reasonToBlockerLabel(reason) {
const r = (reason || '').toLowerCase();
if (r.includes('ci not green') || r.includes('sdk code changed but no ci')) return 'pipeline/blocked:ci';
if (
r.includes('conflict') ||
r.includes('mergestate=dirty') ||
r.includes('not mergeable')
) return 'pipeline/blocked:conflict';
if (
r.includes('manual') ||
r.includes('requires manual') ||
r.includes('sensitive path') ||
r.includes('no-auto-merge') ||
r.includes('manual-only label') ||
r.includes('without test') ||
r.includes('product code') ||
r.includes('files changed') ||
r.includes('possible secret')
) return 'pipeline/blocked:manual-review';
if (r.includes('recent @claude') || r.includes('post-push buffer')) return 'pipeline/blocked:cooldown';
if (r.includes('stale final')) return 'pipeline/blocked:stale-final';
if (r.includes('no final claude')) return 'pipeline/blocked:no-final';
if (r.includes('final claude not complete')) return 'pipeline/blocked:stale-final';
if (r.includes('claude.yml in progress') || r.includes('claude-merge-gate-active')) {
return null;
}
return null;
}
function deriveBlockerLabels(evalResult) {
if (evalResult.ready) return [];
const labels = new Set();
for (const reason of evalResult.reasons || []) {
const mapped = reasonToBlockerLabel(reason);
if (mapped && mapped.startsWith('pipeline/blocked:')) labels.add(mapped);
}
return [...labels];
}
function computePipelineLabels(comments, evalResult) {
const stage = deriveStage(comments, evalResult);
const blockers = stage === 'pipeline/merge-ready' ? [] : deriveBlockerLabels(evalResult);
return { stage, blockers, all: [stage, ...blockers] };
}
async function ensurePipelineLabels(github, owner, repo, core) {
let existing;
if (typeof github.paginate === 'function') {
existing = await github.paginate(github.rest.issues.listLabelsForRepo, {
owner,
repo,
per_page: 100,
});
} else {
({ data: existing } = await github.rest.issues.listLabelsForRepo({
owner,
repo,
per_page: 100,
}));
}
const names = new Set(existing.map((l) => l.name));
for (const spec of LABEL_SPECS) {
if (names.has(spec.name)) continue;
try {
await github.rest.issues.createLabel({
owner,
repo,
name: spec.name,
color: spec.color,
description: spec.description,
});
core?.info?.(`Created label ${spec.name}`);
} catch (err) {
if (err.status !== 422) throw err;
core?.info?.(`Label ${spec.name} already exists`);
}
}
}
async function syncPipelineLabels(github, owner, repo, prNumber, core) {
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
if (ctx.pr.state !== 'open') return { synced: false, reason: 'not_open' };
const evalResult = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core
);
const { stage, blockers, all } = computePipelineLabels(ctx.comments, evalResult);
const current = ctx.labels.filter((l) => l.startsWith(PIPELINE_PREFIX));
const desired = new Set(all);
const toRemove = current.filter((l) => !desired.has(l) && l !== 'pipeline/merged');
const toAdd = all.filter((l) => !current.includes(l));
for (const name of toRemove) {
try {
await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name });
} catch (err) {
if (err.status !== 404) throw err;
}
}
if (toAdd.length) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: toAdd,
});
}
core?.info?.(`PR #${prNumber}: stage=${stage} blockers=[${blockers.join(', ')}]`);
const ciNotGreen = (evalResult.reasons || []).some((r) =>
r.toLowerCase().includes('ci not green')
);
if (ciNotGreen || blockers.includes('pipeline/blocked:ci')) {
await ciFix.maybeTriggerCiFixClaude(github, owner, repo, prNumber, core);
} else {
await ciFix.maybeClearCiFixLabel(
github, owner, repo, prNumber, ctx.labels, ctx.headSha, core
);
}
return {
synced: true,
stage,
blockers,
ready: evalResult.ready,
reasons: evalResult.reasons,
labels: ctx.labels,
createdAt: new Date(ctx.pr.created_at).getTime(),
};
}
async function dispatchMergeGateForOldestReady(github, owner, repo, readyCandidates, core) {
if (!readyCandidates.length) return 0;
readyCandidates.sort((a, b) => a.createdAt - b.createdAt);
for (const cand of readyCandidates) {
if ((cand.labels || []).includes('claude-merge-gate-active')) {
core?.info?.(`Skip dispatch PR #${cand.prNumber}: merge gate already active`);
continue;
}
await github.rest.repos.createDispatchEvent({
owner,
repo,
event_type: 'claude-merge-gate',
client_payload: { pr_number: cand.prNumber },
});
core?.info?.(`Dispatched merge gate for ready PR #${cand.prNumber}`);
return cand.prNumber;
}
return 0;
}
async function syncOpenPullRequests(github, owner, repo, options, core) {
const { maxPrs = 20, dispatchMergeGate = true } = options || {};
await ensurePipelineLabels(github, owner, repo, core);
let prs;
if (maxPrs <= 100) {
({ data: prs } = await github.rest.pulls.list({
owner,
repo,
state: 'open',
per_page: 100,
}));
} else {
prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
per_page: 100,
});
}
let synced = 0;
const readyCandidates = [];
for (const pr of prs) {
if (synced >= maxPrs) break;
if (pr.draft) continue;
if (pr.head?.repo?.full_name && pr.head.repo.full_name !== `${owner}/${repo}`) continue;
const result = await syncPipelineLabels(github, owner, repo, pr.number, core);
if (result.ready) {
readyCandidates.push({
prNumber: pr.number,
createdAt: result.createdAt || new Date(pr.created_at).getTime(),
labels: result.labels || [],
});
}
synced += 1;
}
let dispatched = 0;
if (dispatchMergeGate && readyCandidates.length) {
dispatched = await dispatchMergeGateForOldestReady(
github, owner, repo, readyCandidates, core
);
}
core?.info?.(`Pipeline label sync complete (${synced} PR(s), dispatched=${dispatched || 'none'})`);
return synced;
}
module.exports = {
STAGE_LABELS,
BLOCKER_LABELS,
ALL_PIPELINE_LABELS,
deriveStage,
deriveBlockerLabels,
computePipelineLabels,
ensurePipelineLabels,
syncPipelineLabels,
syncOpenPullRequests,
dispatchMergeGateForOldestReady,
};
@@ -0,0 +1,90 @@
#!/usr/bin/env node
const chain = require('./pr-review-chain.js');
let failed = 0;
function assert(name, cond) {
if (!cond) {
console.error('FAIL:', name);
failed++;
} else {
console.log('ok:', name);
}
}
const coderabbit = {
user: { login: 'coderabbitai[bot]' },
body: '<!-- summarize by coderabbit.ai --> summary',
created_at: '2026-07-02T07:16:00Z',
};
const greptile = {
user: { login: 'greptile-apps[bot]' },
body: '<h3>Greptile Summary</h3><p>Looks good</p>',
created_at: '2026-07-02T07:19:00Z',
};
const copilotKick = {
user: { login: 'MervinPraison' },
body: '@copilot Do a thorough review',
created_at: '2026-07-02T07:20:00Z',
};
const copilotReply = {
user: { login: 'copilot-swe-agent' },
body: 'Copilot review here',
created_at: '2026-07-02T07:22:00Z',
};
assert('prior not ready without greptile', !chain.priorReviewersReady([coderabbit]).ready);
assert('prior ready with coderabbit + greptile', chain.priorReviewersReady([coderabbit, greptile]).ready);
assert('copilot not ready before trigger', !chain.copilotReviewReady([coderabbit, greptile]).ready);
assert(
'copilot ready after reply',
chain.copilotReviewReady([coderabbit, greptile, copilotKick, copilotReply]).ready
);
assert(
'claude not ready before copilot',
!chain.claudeFinalReady([coderabbit, greptile]).ready
);
assert(
'claude ready after full chain',
chain.claudeFinalReady([coderabbit, greptile, copilotKick, copilotReply]).ready
);
assert(
'claude timeout fallback when copilot triggered but silent',
chain.claudeFinalReady([coderabbit, greptile, copilotKick], [], { allowCopilotTimeout: true }).ready
);
assert(
'no timeout fallback without copilot trigger',
!chain.claudeFinalReady([coderabbit, greptile], [], { allowCopilotTimeout: true }).ready
);
assert(
'skipCopilot proceeds after prior reviewers',
chain.claudeFinalReady([coderabbit, greptile], [], { skipCopilot: true }).ready
);
const oldPr = new Date(Date.now() - 31 * 60 * 1000).toISOString();
assert(
'prior not ready without coderabbit before timeout',
!chain.priorReviewersReady([greptile], [], { prCreatedAt: new Date().toISOString(), requiredWaitMs: 30 * 60 * 1000 }).ready
);
assert(
'prior timeout when coderabbit silent after 30m',
chain.priorReviewersReady([greptile], [], { prCreatedAt: oldPr, requiredWaitMs: 30 * 60 * 1000 }).ready
);
assert(
'claude FINAL timeout when required reviewer silent and no prior FINAL',
chain.claudeFinalReady([greptile], [], { prCreatedAt: oldPr, requiredWaitMs: 30 * 60 * 1000 }).ready
);
assert(
'claude FINAL timeout auto-skips copilot without explicit skipCopilot',
chain.claudeFinalReady([greptile], [], { prCreatedAt: oldPr, requiredWaitMs: 30 * 60 * 1000 }).copilotSkipped
);
const finalClaude = {
user: { login: 'github-actions[bot]' },
body: '@claude You are the FINAL architecture reviewer for this PR.',
created_at: '2026-07-02T07:25:00Z',
};
assert(
'claude FINAL not re-posted when already triggered',
!chain.claudeFinalReady([greptile, finalClaude], [], { prCreatedAt: oldPr, requiredWaitMs: 0 }).ready
);
process.exit(failed ? 1 : 0);
+319
View File
@@ -0,0 +1,319 @@
/**
* PR review pipeline: CodeRabbit / Greptile (+ optional Qodo/Gemini) → Claude (FINAL).
* Copilot step optional — set REVIEW_CHAIN_SKIP_COPILOT=1 or skipCopilot: true to bypass.
* @see .github/workflows/auto-pr-comment.yml
*/
function isSkipCopilot(options = {}) {
if (options.skipCopilot === true) return true;
if (options.skipCopilot === false) return false;
return process.env.REVIEW_CHAIN_SKIP_COPILOT === '1';
}
const COPILOT_TRIGGER_LOGINS = new Set(['MervinPraison', 'github-actions[bot]']);
const CLAUDE_TRIGGER_LOGINS = new Set(['MervinPraison', 'github-actions[bot]']);
const REQUIRED_PRIOR = ['coderabbit', 'greptile'];
const OPTIONAL_PRIOR = ['qodo', 'gemini'];
const OPTIONAL_PRIOR_WAIT_MS = 20 * 60 * 1000;
const REQUIRED_PRIOR_WAIT_MS = 30 * 60 * 1000;
function loginOf(item) {
return (item?.user?.login || '').toLowerCase();
}
function bodyOf(item) {
return item?.body || '';
}
function hasCoderabbitSummary(comments) {
return comments.some(
(c) =>
loginOf(c).includes('coderabbit') &&
bodyOf(c).toLowerCase().includes('summarize by coderabbit')
);
}
function hasGreptileReview(comments) {
return comments.some((c) => {
if (!loginOf(c).includes('greptile')) return false;
const body = bodyOf(c);
return body.includes('Greptile Summary') || body.includes('<h3>Greptile Summary</h3>') || body.length > 150;
});
}
function hasQodoReview(comments, reviews = []) {
if (comments.some((c) => loginOf(c).includes('qodo'))) return true;
return reviews.some((r) => loginOf(r).includes('qodo'));
}
function hasGeminiReview(comments, reviews = []) {
if (reviews.some((r) => loginOf(r).includes('gemini'))) return true;
return comments.some((c) => {
if (loginOf(c).includes('gemini')) return true;
const body = bodyOf(c);
return (
body.includes('Review completed by Gemini CLI') ||
body.includes('The fix is ready for review. Please test')
);
});
}
const PRIOR_CHECKS = {
coderabbit: (comments) => hasCoderabbitSummary(comments),
greptile: (comments) => hasGreptileReview(comments),
qodo: (comments, reviews) => hasQodoReview(comments, reviews),
gemini: (comments, reviews) => hasGeminiReview(comments, reviews),
};
function priorReviewerStatus(comments, reviews = []) {
const status = {};
for (const [id, check] of Object.entries(PRIOR_CHECKS)) {
status[id] = check(comments, reviews);
}
return status;
}
function priorReviewersReady(comments, reviews = [], options = {}) {
const waitMs = options.optionalWaitMs ?? OPTIONAL_PRIOR_WAIT_MS;
const requiredWaitMs = options.requiredWaitMs ?? REQUIRED_PRIOR_WAIT_MS;
const prCreatedAt = options.prCreatedAt || null;
const status = priorReviewerStatus(comments, reviews);
const missingRequired = REQUIRED_PRIOR.filter((id) => !status[id]);
if (missingRequired.length) {
if (prCreatedAt) {
const age = Date.now() - new Date(prCreatedAt).getTime();
if (age >= requiredWaitMs) {
return {
ready: true,
reason: `required reviewer timeout (${missingRequired.join(', ')} silent after ${Math.round(requiredWaitMs / 60000)}m)`,
status,
requiredTimeout: true,
};
}
}
return { ready: false, reason: `waiting for ${missingRequired.join(', ')}`, status };
}
const missingOptional = OPTIONAL_PRIOR.filter((id) => !status[id]);
if (missingOptional.length && prCreatedAt) {
const age = Date.now() - new Date(prCreatedAt).getTime();
if (age < waitMs) {
return {
ready: false,
reason: `waiting for optional ${missingOptional.join(', ')} (${Math.round((waitMs - age) / 60000)}m left)`,
status,
};
}
}
return { ready: true, reason: '', status };
}
function copilotTriggerComment(comments) {
return comments.find(
(c) =>
bodyOf(c).includes('@copilot') &&
COPILOT_TRIGGER_LOGINS.has(c.user?.login) &&
!bodyOf(c).toLowerCase().includes('@claude')
);
}
function copilotTriggered(comments) {
return Boolean(copilotTriggerComment(comments));
}
function copilotReviewReady(comments, reviews = []) {
const trigger = copilotTriggerComment(comments);
if (!trigger) {
return { ready: false, reason: 'copilot not triggered yet' };
}
const triggerTime = new Date(trigger.created_at).getTime();
const copilotComment = comments.some((c) => {
if (c === trigger) return false;
if (new Date(c.created_at).getTime() < triggerTime) return false;
const login = loginOf(c);
return (
login.includes('copilot') ||
c.user?.login === 'Copilot' ||
c.user?.login === 'copilot-swe-agent'
);
});
const copilotReview = reviews.some((r) => {
if (new Date(r.submitted_at).getTime() < triggerTime) return false;
return loginOf(r).includes('copilot');
});
if (copilotComment || copilotReview) {
return { ready: true, reason: '' };
}
return { ready: false, reason: 'awaiting copilot review' };
}
function isFinalClaudeTriggerComment(c) {
const body = bodyOf(c).toLowerCase();
if (!CLAUDE_TRIGGER_LOGINS.has(c.user?.login)) return false;
if (!body.includes('@claude')) return false;
if (body.includes('merge conflict')) return false;
return body.includes('final architecture reviewer') || body.includes('lead engineer');
}
function claudeFinalAlreadyTriggered(comments) {
return comments.some(isFinalClaudeTriggerComment);
}
function claudeFinalReady(comments, reviews = [], options = {}) {
if (!options.allowStaleRepost && claudeFinalAlreadyTriggered(comments)) {
return { ready: false, reason: 'claude FINAL already triggered', already: true };
}
const prior = priorReviewersReady(comments, reviews, options);
if (!prior.ready) {
return { ready: false, reason: prior.reason };
}
if (isSkipCopilot(options) || prior.requiredTimeout) {
return {
ready: true,
reason: prior.requiredTimeout ? prior.reason : 'copilot skipped',
copilotSkipped: true,
requiredTimeout: prior.requiredTimeout,
};
}
const copilot = copilotReviewReady(comments, reviews);
if (copilot.ready) {
return { ready: true, reason: '' };
}
if (options.allowCopilotTimeout && copilotTriggered(comments)) {
return { ready: true, reason: 'copilot timeout fallback', copilotSkipped: true };
}
return { ready: false, reason: copilot.reason };
}
/** Post @copilot when prior reviewers are ready, then @claude FINAL when Copilot done (or timeout). */
async function advanceReviewChain(github, owner, repo, prNumber, finalBody, core, options = {}) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
const chainOpts = {
prCreatedAt: pr.created_at,
optionalWaitMs: options.optionalWaitMs ?? 0,
};
const copilot = options.skipCopilot
? { triggered: false, reason: 'skipped' }
: await maybeTriggerCopilot(github, owner, repo, prNumber, core, chainOpts);
const claude = await maybeTriggerClaudeFinal(
github, owner, repo, prNumber, finalBody, core,
{ ...options, ...chainOpts }
);
return { copilot, claude };
}
async function maybeTriggerClaudeFinal(github, owner, repo, prNumber, finalBody, core, options = {}) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
const { comments, reviews } = await listCommentsAndReviews(github, owner, repo, prNumber);
const gate = claudeFinalReady(comments, reviews, {
prCreatedAt: pr.created_at,
optionalWaitMs: options.optionalWaitMs,
allowCopilotTimeout: options.allowCopilotTimeout !== false,
allowStaleRepost: options.allowStaleRepost === true,
skipCopilot: options.skipCopilot,
});
if (!gate.ready) {
core?.info?.(`PR #${prNumber}: skip Claude FINAL — ${gate.reason}`);
return { posted: false, reason: gate.reason };
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: finalBody,
});
const note = gate.copilotSkipped ? ' (Copilot timeout fallback)' : '';
core?.info?.(`Posted Claude FINAL on PR #${prNumber}${note}`);
return { posted: true, reason: '' };
}
const COPILOT_REVIEW_BODY =
'@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, CodeRabbit, Gemini, and Greptile first — incorporate their findings.\n\nReview areas:\n1. **Bloat check**: Are changes minimal and focused? Any unnecessary code or scope creep?\n2. **Security**: Any hardcoded secrets, unsafe eval/exec, missing input validation?\n3. **Performance**: Any module-level heavy imports? Hot-path regressions?\n4. **Tests**: Are tests included? Do they cover the changes adequately?\n5. **Backward compat**: Any public API changes without deprecation?\n6. **Code quality**: DRY violations, naming conventions, error handling?\n7. **Address reviewer feedback**: If Qodo, CodeRabbit, Gemini, or Greptile flagged valid issues, include them in your review\n8. Suggest specific improvements with code examples where possible';
async function listCommentsAndReviews(github, owner, repo, prNumber) {
let comments;
if (typeof github.paginate === 'function') {
comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
} else {
const { data } = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
comments = data;
}
const { data: reviews } = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
return { comments, reviews };
}
async function maybeTriggerCopilot(github, owner, repo, prNumber, core, options = {}) {
const { comments, reviews } = await listCommentsAndReviews(github, owner, repo, prNumber);
if (copilotTriggered(comments)) {
core?.info?.(`Copilot already triggered on PR #${prNumber}`);
return { triggered: false, reason: 'already_triggered' };
}
const prior = priorReviewersReady(comments, reviews, options);
if (!prior.ready) {
core?.info?.(`PR #${prNumber}: not ready for Copilot — ${prior.reason}`);
return { triggered: false, reason: prior.reason };
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: COPILOT_REVIEW_BODY,
});
core?.info?.(`Posted @copilot on PR #${prNumber} after prior reviewers`);
return { triggered: true };
}
async function pollCopilotResponse(github, owner, repo, prNumber, options = {}) {
const maxAttempts = options.maxAttempts ?? 20;
const delayMs = options.delayMs ?? 30000;
for (let i = 0; i < maxAttempts; i += 1) {
const { comments, reviews } = await listCommentsAndReviews(github, owner, repo, prNumber);
const copilot = copilotReviewReady(comments, reviews);
if (copilot.ready) {
return { ready: true, reason: '' };
}
if (i < maxAttempts - 1) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
return { ready: false, reason: 'copilot timeout' };
}
module.exports = {
isSkipCopilot,
REQUIRED_PRIOR,
OPTIONAL_PRIOR,
REQUIRED_PRIOR_WAIT_MS,
COPILOT_REVIEW_BODY,
priorReviewerStatus,
priorReviewersReady,
copilotTriggered,
copilotReviewReady,
claudeFinalReady,
claudeFinalAlreadyTriggered,
isFinalClaudeTriggerComment,
maybeTriggerCopilot,
maybeTriggerClaudeFinal,
advanceReviewChain,
pollCopilotResponse,
hasGeminiReview,
listCommentsAndReviews,
};
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env node
/**
* Run: node .github/scripts/release-gate-selftest.js
*/
const rg = require('./release-gate.js');
const config = require('./gate-config.js');
let failed = 0;
function assert(name, cond) {
if (!cond) {
console.error('FAIL:', name);
failed += 1;
} else {
console.log('ok:', name);
}
}
assert('package name is aiui', config.pypiPackageName === 'aiui');
assert('bumpPatch works', rg.bumpPatch('0.3.127') === '0.3.128');
try {
const versions = rg.readVersionsFromTree(process.cwd());
assert('reads current version', /^\d+\.\d+\.\d+$/.test(versions.current));
assert('computes target patch', versions.target === rg.bumpPatch(versions.current));
} catch (err) {
console.error('FAIL: readVersionsFromTree', err.message);
failed += 1;
}
assert('PACKAGE_PATHS includes pyproject', rg.PACKAGE_PATHS.includes('pyproject.toml'));
process.exit(failed ? 1 : 0);
+205
View File
@@ -0,0 +1,205 @@
/**
* Release gate preflight — path changes, CI SHA, dedupe, PyPI version checks (PraisonAIUI).
*/
const https = require('https');
const config = require('./gate-config.js');
const PACKAGE_PATHS = config.packagePaths;
const ACTIVE_RELEASE_STATUSES = new Set([
'queued', 'in_progress', 'waiting', 'pending', 'requested',
]);
function bumpPatch(version) {
const parts = version.split('.');
if (parts.length !== 3) throw new Error(`Invalid version: ${version}`);
return `${parts[0]}.${parts[1]}.${Number(parts[2]) + 1}`;
}
function readVersionsFromTree(root = '.') {
const fs = require('fs');
const path = require('path');
const toml = fs.readFileSync(path.join(root, 'pyproject.toml'), 'utf8');
const match = toml.match(/^version\s*=\s*"([^"]+)"/m);
if (!match) throw new Error('Could not read aiui version from pyproject.toml');
const current = match[1];
const target = bumpPatch(current);
return { current, target, packageName: config.pypiPackageName };
}
function pypiVersionExists(packageName, version) {
return new Promise((resolve) => {
const req = https.get(
`https://pypi.org/pypi/${packageName}/${version}/json`,
{ timeout: 15000 },
(res) => {
res.resume();
resolve(res.statusCode === 200);
}
);
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
});
}
async function hasActiveReleaseRun(github, owner, repo) {
const runs = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'pypi-release.yml',
per_page: 20,
});
return runs.data.workflow_runs.some(
(r) => ACTIVE_RELEASE_STATUSES.has(r.status) && !r.conclusion
);
}
function utcDayStart(now = new Date()) {
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
}
async function hasSuccessfulReleaseToday(github, owner, repo, now = new Date()) {
const dayStart = utcDayStart(now);
const runs = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'pypi-release.yml',
status: 'completed',
per_page: 30,
});
return runs.data.workflow_runs.some(
(r) => r.conclusion === 'success' && new Date(r.created_at) >= dayStart
);
}
async function lastGreenCiSha(github, owner, repo) {
const runs = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: config.ciWorkflowFile,
branch: 'main',
status: 'completed',
per_page: 30,
});
const hit = runs.data.workflow_runs.find((r) => r.conclusion === 'success');
return hit ? hit.head_sha : '';
}
async function evaluateReleasePreflight(github, owner, repo, options, core) {
const {
headSha,
isCiTrigger = false,
bump = 'patch',
} = options;
const reasons = [];
const out = {
ready: false,
reasons,
headSha: headSha || '',
lastTag: '',
targetVersion: '',
packageName: config.pypiPackageName,
};
if (bump !== 'patch') {
reasons.push('only patch auto-release supported');
return out;
}
if (await hasActiveReleaseRun(github, owner, repo)) {
reasons.push('PyPI Release already in progress or awaiting approval');
return out;
}
const referenceTime = options.now instanceof Date ? options.now : new Date();
if (await hasSuccessfulReleaseToday(github, owner, repo, referenceTime)) {
const day = referenceTime.toISOString().slice(0, 10);
reasons.push(`already released today (UTC ${day}); max one patch release per day`);
return out;
}
let versions;
try {
versions = readVersionsFromTree();
} catch (err) {
reasons.push(err.message);
return out;
}
out.targetVersion = versions.target;
const onPypi = await pypiVersionExists(config.pypiPackageName, versions.target);
if (onPypi) {
reasons.push(`already published: ${config.pypiPackageName}==${versions.target}`);
return out;
}
const { execSync } = require('child_process');
let lastTag = '';
try {
lastTag = execSync('git describe --tags --match "v*" --abbrev=0', { encoding: 'utf8' }).trim();
} catch {
reasons.push('no v* tag found');
return out;
}
out.lastTag = lastTag;
let changed = '';
try {
changed = execSync(
`git diff --name-only ${lastTag} HEAD -- ${PACKAGE_PATHS.join(' ')}`,
{ encoding: 'utf8' }
).trim();
} catch {
reasons.push('git diff failed');
return out;
}
if (!changed) {
reasons.push(`no changes in ${PACKAGE_PATHS.join(' or ')} since ${lastTag}`);
return out;
}
if (isCiTrigger) {
let mainSha = '';
try {
mainSha = execSync('git rev-parse origin/main', { encoding: 'utf8' }).trim();
} catch {
reasons.push('could not resolve origin/main');
return out;
}
if (headSha !== mainSha) {
reasons.push(`superseded: green SHA ${headSha.slice(0, 7)} != main ${mainSha.slice(0, 7)}`);
return out;
}
out.headSha = headSha;
} else {
const evalSha = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
out.headSha = evalSha;
const greenSha = await lastGreenCiSha(github, owner, repo);
if (greenSha !== evalSha) {
reasons.push(`CI not green on HEAD (last green: ${greenSha ? greenSha.slice(0, 7) : 'none'})`);
return out;
}
}
out.ready = true;
out.reasons = ['ready'];
if (core) core.info(`Release preflight passed for ${out.headSha || headSha}`);
return out;
}
module.exports = {
bumpPatch,
readVersionsFromTree,
pypiVersionExists,
hasSuccessfulReleaseToday,
utcDayStart,
evaluateReleasePreflight,
lastGreenCiSha,
ACTIVE_RELEASE_STATUSES,
PACKAGE_PATHS,
};
+26 -17
View File
@@ -1,8 +1,7 @@
name: Auto Issue Comment
# Optional: automatically escalate every new issue to Claude for a full
# investigation + draft fix. Disabled by default (only workflow_dispatch).
# To enable auto-triage on every new issue, uncomment the `issues:` trigger.
# Automatically escalate every new issue to Claude for a full investigation
# + draft fix. Manual re-trigger: workflow_dispatch with issue_number.
#
# Note: `@claude` mentions only trigger claude.yml when the commenter is a
# HUMAN user (or github-actions[bot] via PAT). This workflow posts the
@@ -14,9 +13,9 @@ on:
issue_number:
description: "Issue number to triage with Claude"
required: true
type: number
# issues:
# types: [opened]
type: string
issues:
types: [opened]
jobs:
# Escalate an issue to full Claude implementation by adding the `claude`
@@ -25,10 +24,11 @@ jobs:
trigger-claude:
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issues' && !contains(toJSON(github.event.issue.labels), 'claude'))
github.event_name == 'issues'
runs-on: ubuntu-latest
permissions:
issues: write
actions: write
steps:
- name: Resolve issue number
id: resolve
@@ -52,18 +52,18 @@ jobs:
});
const labels = (issue.labels || []).map(l => typeof l === 'string' ? l : l.name);
if (labels.includes('claude')) {
console.log(`Issue #${issueNumber} already has 'claude' label — skipping.`);
return;
const hadClaude = labels.includes('claude');
if (!hadClaude) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ['claude'],
});
} else {
console.log(`Issue #${issueNumber} already has 'claude' label — dispatching Claude only.`);
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ['claude'],
});
const body = [
'🤖 Auto-triaging with Claude.',
'',
@@ -84,3 +84,12 @@ jobs:
issue_number: issueNumber,
body,
});
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'claude.yml',
ref: 'main',
inputs: { issue_number: String(issueNumber) },
});
console.log(`Dispatched Claude Assistant for issue #${issueNumber}`);
+18 -510
View File
@@ -1,525 +1,33 @@
name: Auto PR Comment
# Review chain: CodeRabbit/Qodo/Gemini review first → Copilot → Claude (final)
# For human PRs: CodeRabbit/Qodo auto-review → this workflow triggers Copilot → then Claude
# For bot PRs: CodeRabbit/Qodo skip bots, so we trigger them explicitly first
#
# IMPORTANT: @copilot ignores bot comments. All @copilot mentions must use
# PAT_TOKEN so the comment posts as MervinPraison (a real user).
#
# Claude trigger runs IN-BAND (same workflow) to avoid GitHub Actions
# blocking bot-triggered workflows with action_required approval gates.
on:
issue_comment:
types: [created]
pull_request_review:
types: [submitted]
pull_request:
types: [opened]
types: [opened, synchronize]
pull_request_target:
types: [opened, synchronize]
schedule:
- cron: '30 3,9,15,21 * * *'
workflow_dispatch:
env:
MAX_POLL_ATTEMPTS: 20
POLL_DELAY_MS: 30000
CLAUDE_TRIGGER_LOGINS: '["MervinPraison","github-actions[bot]"]'
REVIEW_CHAIN_SKIP_COPILOT: '1'
permissions:
issues: write
pull-requests: write
jobs:
# ================================================================
# HUMAN PRs: Trigger @copilot AFTER CodeRabbit posts its summary
# ================================================================
copilot-after-coderabbit:
if: |
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
github.event.comment.user.login == 'coderabbitai[bot]' &&
contains(github.event.comment.body, 'summarize by coderabbit')
runs-on: ubuntu-latest
permissions:
pull-requests: write
outputs:
pr_number: ${{ steps.trigger.outputs.pr_number }}
triggered: ${{ steps.trigger.outputs.triggered }}
steps:
- name: Post Copilot review request (as user via PAT)
id: trigger
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const comments = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
// Check for duplicates — posted by MervinPraison (PAT) or github-actions[bot] (legacy)
const alreadyPosted = comments.data.some(c =>
c.body.includes('@copilot') &&
(c.user.login === 'MervinPraison' || c.user.login === 'github-actions[bot]')
);
if (alreadyPosted) {
console.log('Copilot already triggered, skipping');
core.setOutput('triggered', 'false');
core.setOutput('pr_number', context.issue.number.toString());
return;
}
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, Coderabbit, and Gemini first — incorporate their findings.\n\nReview areas:\n1. **Bloat check**: Are changes minimal and focused? Any unnecessary code or scope creep?\n2. **Security**: Any hardcoded secrets, unsafe eval/exec, missing input validation?\n3. **Performance**: Any module-level heavy imports? Hot-path regressions?\n4. **Tests**: Are tests included? Do they cover the changes adequately?\n5. **Backward compat**: Any public API changes without deprecation?\n6. **Code quality**: DRY violations, naming conventions, error handling?\n7. **Address reviewer feedback**: If Qodo, Coderabbit, or Gemini flagged valid issues, include them in your review\n8. Suggest specific improvements with code examples where possible'
});
core.setOutput('triggered', 'true');
core.setOutput('pr_number', context.issue.number.toString());
# Fallback: Trigger @copilot after Qodo review (if Coderabbit doesn't fire)
copilot-after-qodo:
if: |
github.event_name == 'pull_request_review' &&
github.event.review.user.login == 'qodo-code-review[bot]'
runs-on: ubuntu-latest
permissions:
pull-requests: write
outputs:
pr_number: ${{ steps.trigger.outputs.pr_number }}
triggered: ${{ steps.trigger.outputs.triggered }}
steps:
- name: Post Copilot review request (as user via PAT)
id: trigger
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const comments = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
const alreadyPosted = comments.data.some(c =>
c.body.includes('@copilot') &&
(c.user.login === 'MervinPraison' || c.user.login === 'github-actions[bot]')
);
if (alreadyPosted) {
console.log('Copilot already triggered, skipping');
core.setOutput('triggered', 'false');
core.setOutput('pr_number', context.issue.number.toString());
return;
}
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above first.\n\nReview areas:\n1. **Bloat check**: Are changes minimal and focused?\n2. **Security**: Any hardcoded secrets, unsafe eval/exec, missing input validation?\n3. **Performance**: Any module-level heavy imports? Hot-path regressions?\n4. **Tests**: Are tests included? Do they cover the changes adequately?\n5. **Backward compat**: Any public API changes without deprecation?\n6. **Code quality**: DRY violations, naming conventions, error handling?\n7. Suggest specific improvements with code examples where possible'
});
core.setOutput('triggered', 'true');
core.setOutput('pr_number', context.issue.number.toString());
# ================================================================
# FALLBACK: Claude trigger for cases where Copilot chain fails/skips
# Ensures Claude always runs after sufficient review time
# ================================================================
claude-fallback-timeout:
if: |
github.event_name == 'pull_request' &&
github.event.action == 'opened'
runs-on: ubuntu-latest
timeout-minutes: 20
review-chain:
# Grant union of callee job permissions (repos with default_workflow_permissions: read
# reject reusable calls when the caller ceiling is lower than any callee job).
permissions:
issues: write
pull-requests: write
steps:
- name: Wait for review window (15 min)
run: sleep 900
- name: Check if Claude already triggered
id: check_claude
uses: actions/github-script@v7
env:
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
const comments = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
const alreadyPosted = comments.data.some(c =>
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
);
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
core.setOutput('comments_count', comments.data.length.toString());
// Collect all bot reviewer comments for context
const botReviews = comments.data.filter(c =>
['coderabbitai[bot]', 'qodo-code-review[bot]', 'gemini-code-assist[bot]',
'copilot-swe-agent', 'Copilot', 'greptile-apps[bot]'].some(bot =>
c.user.login.toLowerCase().includes(bot.toLowerCase())
)
);
core.setOutput('review_count', botReviews.length.toString());
- name: Aggregate reviews and trigger Claude
if: steps.check_claude.outputs.already_triggered != 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const comments = await github.rest.issues.listComments({issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, per_page: 100});
const reviews = {coderabbit: [], qodo: [], gemini: [], copilot: [], greptile: [], others: []};
comments.data.forEach(c => {
const login = c.user.login.toLowerCase();
const body = c.body.substring(0, 500);
if (login.includes('coderabbit')) reviews.coderabbit.push(body);
else if (login.includes('qodo')) reviews.qodo.push(body);
else if (login.includes('gemini')) reviews.gemini.push(body);
else if (login.includes('copilot')) reviews.copilot.push(body);
else if (login.includes('greptile')) reviews.greptile.push(body);
else if (c.user.type === 'Bot') reviews.others.push(body);
});
const summaryParts = [];
if (reviews.coderabbit.length) summaryParts.push('CodeRabbit: ' + reviews.coderabbit.length + ' comments');
if (reviews.qodo.length) summaryParts.push('Qodo: ' + reviews.qodo.length + ' comments');
if (reviews.gemini.length) summaryParts.push('Gemini: ' + reviews.gemini.length + ' comments');
if (reviews.copilot.length) summaryParts.push('Copilot: ' + reviews.copilot.length + ' comments');
if (reviews.greptile.length) summaryParts.push('Greptile: ' + reviews.greptile.length + ' comments');
const reviewSummary = summaryParts.length ? summaryParts.join(' | ') : 'No bot reviews detected';
const claudeBody = '@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAIUI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on PraisonAIUI (src/praisonaiui, tests, docs). Do not expand scope into praisonaiagents or the monorepo unless the issue explicitly requires it. ' + reviewSummary + '.\n\n' +
'Review Context: ' + comments.data.length + ' total comments, ' +
(reviews.coderabbit.length + reviews.qodo.length + reviews.gemini.length + reviews.copilot.length + reviews.greptile.length) + ' bot reviews.\n\n' +
'Phase 1: Review per AGENTS.md principles (protocol-driven, backward compatible, no perf impact).\n' +
'Phase 2: FIX valid issues found by prior reviewers.\n' +
'Phase 3: Provide final verdict (approve or request changes).';
await github.rest.issues.createComment({issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: claudeBody});
console.log('Claude fallback triggered on PR #' + context.issue.number + ' with ' + reviewSummary);
# ================================================================
# Claude FINAL review — polls for Copilot's response, then triggers
# This runs IN-BAND under the PAT context (MervinPraison), avoiding
# the action_required approval gate that blocks bot-triggered workflows.
# ================================================================
claude-after-copilot:
needs: [copilot-after-coderabbit]
if: |
always() &&
needs.copilot-after-coderabbit.result == 'success' &&
needs.copilot-after-coderabbit.outputs.triggered == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
issues: write
pull-requests: write
steps:
- name: Wait for Copilot to respond (poll)
id: poll
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
MAX_POLL_ATTEMPTS: ${{ env.MAX_POLL_ATTEMPTS }}
POLL_DELAY_MS: ${{ env.POLL_DELAY_MS }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const owner = context.repo.owner;
const repo = context.repo.repo;
// Poll for Copilot's response — check every 30s for up to 10 minutes
const maxAttempts = parseInt(process.env.MAX_POLL_ATTEMPTS, 10);
const delayMs = parseInt(process.env.POLL_DELAY_MS, 10);
for (let i = 0; i < maxAttempts; i++) {
console.log(`Poll attempt ${i + 1}/${maxAttempts}...`);
const comments = await github.rest.issues.listComments({
issue_number: pr,
owner,
repo,
per_page: 50,
sort: 'created',
direction: 'desc'
});
// Look for Copilot's response (copilot-swe-agent or Copilot)
const copilotResponse = comments.data.find(c =>
(c.user.login === 'copilot-swe-agent' ||
c.user.login === 'Copilot' ||
c.user.login.toLowerCase().includes('copilot')) &&
c.user.type === 'Bot'
);
if (copilotResponse) {
console.log(`Copilot responded at ${copilotResponse.created_at}`);
core.setOutput('copilot_responded', 'true');
return;
}
// Also check PR reviews for Copilot
const reviews = await github.rest.pulls.listReviews({
pull_number: pr,
owner,
repo
});
const copilotReview = reviews.data.find(r =>
r.user.login.toLowerCase().includes('copilot')
);
if (copilotReview) {
console.log(`Copilot review submitted at ${copilotReview.submitted_at}`);
core.setOutput('copilot_responded', 'true');
return;
}
if (i < maxAttempts - 1) {
console.log(`Copilot hasn't responded yet, waiting ${delayMs/1000}s...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
console.log('Copilot did not respond within timeout, triggering Claude anyway');
core.setOutput('copilot_responded', 'timeout');
- name: Check if Claude already triggered
id: check_claude
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
const comments = await github.rest.issues.listComments({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo
});
const alreadyPosted = comments.data.some(c =>
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
);
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
- name: Trigger Claude final review
if: steps.check_claude.outputs.already_triggered != 'true'
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
await github.rest.issues.createComment({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAIUI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on PraisonAIUI (src/praisonaiui, tests, docs). Do not expand scope into praisonaiagents or the monorepo unless the issue explicitly requires it. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n\n**Phase 2: FIX Valid Issues**\n4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n5. Push all code fixes directly to THIS branch (do NOT create a new PR)\n6. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n7. If all issues are resolved, approve the PR / close the Issue\n8. If blocking issues remain, request changes / leave clear action items'
});
console.log(`Claude triggered on PR #${pr}`);
# ================================================================
# Claude after Qodo fallback path (same polling approach)
# ================================================================
claude-after-copilot-qodo:
needs: [copilot-after-qodo]
if: |
always() &&
needs.copilot-after-qodo.result == 'success' &&
needs.copilot-after-qodo.outputs.triggered == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
issues: write
pull-requests: write
steps:
- name: Wait for Copilot to respond (poll)
id: poll
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
MAX_POLL_ATTEMPTS: ${{ env.MAX_POLL_ATTEMPTS }}
POLL_DELAY_MS: ${{ env.POLL_DELAY_MS }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const owner = context.repo.owner;
const repo = context.repo.repo;
const maxAttempts = parseInt(process.env.MAX_POLL_ATTEMPTS, 10);
const delayMs = parseInt(process.env.POLL_DELAY_MS, 10);
for (let i = 0; i < maxAttempts; i++) {
console.log(`Poll attempt ${i + 1}/${maxAttempts}...`);
const comments = await github.rest.issues.listComments({
issue_number: pr, owner, repo,
per_page: 50, sort: 'created', direction: 'desc'
});
const copilotResponse = comments.data.find(c =>
(c.user.login === 'copilot-swe-agent' ||
c.user.login === 'Copilot' ||
c.user.login.toLowerCase().includes('copilot')) &&
c.user.type === 'Bot'
);
if (copilotResponse) {
console.log(`Copilot responded at ${copilotResponse.created_at}`);
core.setOutput('copilot_responded', 'true');
return;
}
const reviews = await github.rest.pulls.listReviews({
pull_number: pr, owner, repo
});
const copilotReview = reviews.data.find(r =>
r.user.login.toLowerCase().includes('copilot')
);
if (copilotReview) {
console.log(`Copilot review submitted at ${copilotReview.submitted_at}`);
core.setOutput('copilot_responded', 'true');
return;
}
if (i < maxAttempts - 1) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
console.log('Copilot did not respond within timeout, triggering Claude anyway');
core.setOutput('copilot_responded', 'timeout');
- name: Check if Claude already triggered
id: check_claude
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
const comments = await github.rest.issues.listComments({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo
});
const alreadyPosted = comments.data.some(c =>
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
);
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
- name: Trigger Claude final review
if: steps.check_claude.outputs.already_triggered != 'true'
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
await github.rest.issues.createComment({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAIUI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on PraisonAIUI (src/praisonaiui, tests, docs). Do not expand scope into praisonaiagents or the monorepo unless the issue explicitly requires it. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n\n**Phase 2: FIX Valid Issues**\n4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n5. Push all code fixes directly to THIS branch (do NOT create a new PR)\n6. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n7. If all issues are resolved, approve the PR / close the Issue\n8. If blocking issues remain, request changes / leave clear action items'
});
console.log(`Claude triggered on PR #${pr}`);
# ================================================================
# Claude after Gemini — restores the trigger lost from the deleted
# chain-claude-after-copilot.yml workflow.
# Fires when Gemini posts a review comment on a PR or issue.
# ================================================================
claude-after-gemini:
if: |
github.event_name == 'issue_comment' &&
(
github.event.comment.user.login == 'gemini-code-assist[bot]' ||
contains(github.event.comment.body, 'Review completed by Gemini CLI') ||
contains(github.event.comment.body, 'The fix is ready for review. Please test')
)
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Check if Claude already triggered
id: check_claude
uses: actions/github-script@v7
env:
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
const comments = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
const alreadyPosted = comments.data.some(c =>
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
);
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
- name: Post Claude analysis request (Lead Engineer)
if: steps.check_claude.outputs.already_triggered != 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude You are the Lead Engineer. If the branch is under MervinPraison/PraisonAIUI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on PraisonAIUI (src/praisonaiui, tests, docs). Do not expand scope into praisonaiagents or the monorepo unless the issue explicitly requires it. Read ALL analysis and reviews above carefully (Gemini, CodeRabbit, Qodo, Copilot, etc).\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n\n**Phase 2: FIX Valid Issues**\n4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n5. Push all code fixes directly to THIS branch (do NOT create a new PR)\n6. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n7. If all issues are resolved, approve the PR / close the Issue\n8. If blocking issues remain, request changes / leave clear action items'
});
# ================================================================
# BOT PRs: CodeRabbit/Qodo skip bot-authored PRs by default.
# Trigger them explicitly. Do NOT trigger Copilot here — it will
# be triggered by copilot-after-coderabbit/qodo when they finish.
# ================================================================
bot-pr-trigger-reviews:
if: |
github.event_name == 'pull_request' &&
github.event.action == 'opened' &&
(
github.event.pull_request.user.login == 'github-actions[bot]' ||
github.event.pull_request.user.login == 'praisonai-triage-agent[bot]' ||
github.event.pull_request.user.type == 'Bot'
)
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Trigger CodeRabbit and Qodo reviews
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const pr = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
// 1. Trigger CodeRabbit review
await github.rest.issues.createComment({
issue_number: pr, owner, repo,
body: '@coderabbitai review'
});
console.log(`Triggered CodeRabbit for bot PR #${pr}`);
// 2. Trigger Qodo review
await github.rest.issues.createComment({
issue_number: pr, owner, repo,
body: '/review'
});
console.log(`Triggered Qodo for bot PR #${pr}`);
// 3. Trigger Gemini review (commented out)
// await github.rest.issues.createComment({
// issue_number: pr, owner, repo,
// body: '@gemini review this PR'
// });
// console.log(`Triggered Gemini for bot PR #${pr}`);
// NOTE: @copilot is NOT triggered here.
// It will be triggered by copilot-after-coderabbit or copilot-after-qodo
// once those bots finish, ensuring Copilot always reviews LAST.
console.log('Copilot will be triggered after CodeRabbit/Qodo complete.');
uses: MervinPraison/github-automation-template/.github/workflows/[email protected]
secrets: inherit
with:
hub_ref: v1.1.6
+23
View File
@@ -0,0 +1,23 @@
name: Bot PR Recovery
on:
schedule:
- cron: '*/15 * * * *'
workflow_dispatch:
inputs:
pr_number:
description: 'Specific PR number to kick (optional)'
required: false
type: string
permissions:
pull-requests: write
issues: write
jobs:
recover:
uses: MervinPraison/github-automation-template/.github/workflows/[email protected]
secrets: inherit
with:
hub_ref: v1.1.6
pr_number: ${{ github.event.inputs.pr_number || '' }}
+32
View File
@@ -0,0 +1,32 @@
name: Auto CI Failure → Claude
on:
workflow_run:
workflows: [Core Tests, Optimized Test Suite]
types: [completed]
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
inputs:
pr_number:
required: false
type: string
schedule:
- cron: '15 */6 * * *'
concurrency:
group: ci-failure-scan-${{ github.repository }}-${{ github.event.pull_request.number || github.event.workflow_run.id || github.run_id }}
cancel-in-progress: true
permissions:
pull-requests: read
issues: write
actions: read
jobs:
detect-and-trigger:
uses: MervinPraison/github-automation-template/.github/workflows/[email protected]
secrets: inherit
with:
hub_ref: v1.1.6
pr_number: ${{ github.event.inputs.pr_number || '' }}
+7 -2
View File
@@ -5,9 +5,11 @@ on:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
python:
name: python
runs-on: ubuntu-latest
strategy:
matrix:
@@ -28,10 +30,12 @@ jobs:
run: ruff check src/praisonaiui
- name: Test with pytest
# Integration tests are environment-sensitive (LLM API keys,
# long-running flows). Unit tests are the contract CI guards.
# Unit tests are the primary CI contract; integration smoke validates backends.
run: pytest tests/unit -v --no-cov --timeout=60
- name: Integration smoke tests
run: pytest tests/integration/test_agentic_roundtrip.py tests/test_feature_sdk_backends.py -q --no-cov --timeout=120
docs:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
@@ -62,3 +66,4 @@ jobs:
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./site
force_orphan: true
+95
View File
@@ -0,0 +1,95 @@
name: Claude PR Merge Gate
on:
schedule:
- cron: '*/10 * * * *'
workflow_dispatch:
inputs:
pr_number:
required: false
type: string
merge_method:
required: false
type: choice
default: merge
options: [merge, squash, rebase]
workflow_run:
workflows:
- Claude Assistant
- Core Tests
types: [completed]
repository_dispatch:
types: [claude-merge-gate]
concurrency:
group: claude-merge-gate-${{ github.repository }}-${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || github.event.workflow_run.id || 'scan' }}
cancel-in-progress: false
permissions:
contents: read
pull-requests: read
issues: read
actions: read
jobs:
auto-dispatch:
if: github.event_name == 'workflow_run'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
actions: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: MervinPraison/github-automation-template/.github/actions/[email protected]
with:
hub_ref: v1.1.6
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Dispatch merge gate when PR pipeline is ready
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.HUB_SCRIPTS, 'merge-gate.js'));
const wr = context.payload.workflow_run;
const owner = context.repo.owner;
const repo = context.repo.repo;
if (wr.conclusion !== 'success') return;
const allowed = ['Claude Assistant', 'Core Tests'];
if (!allowed.includes(wr.name)) return;
const prNumber = await mergeGate.resolvePrNumberFromWorkflowRun(github, owner, repo, wr);
if (!prNumber) return;
const fromClaude = wr.name === 'Claude Assistant';
const check = await mergeGate.evaluatePipelineQuiescent(github, owner, repo, prNumber, core, {
skipGlobalClaudeRunCheck: fromClaude,
skipRecentClaudeCooldown: fromClaude,
});
if (!check.ready) return;
await github.rest.repos.createDispatchEvent({
owner, repo, event_type: 'claude-merge-gate',
client_payload: { pr_number: prNumber },
});
merge-gate:
if: github.event_name != 'workflow_run'
permissions:
contents: write
pull-requests: write
issues: write
actions: read
id-token: write
uses: MervinPraison/github-automation-template/.github/workflows/[email protected]
secrets: inherit
with:
hub_ref: v1.1.6
pr_number: ${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || '' }}
merge_method: ${{ github.event.inputs.merge_method || 'merge' }}
+10 -277
View File
@@ -1,291 +1,24 @@
name: Claude Assistant
# Triggers:
# - issues:opened → lightweight triage (welcome comment + label)
# - issues:labeled → full implementation when `claude` label applied
# - issues:assigned → full implementation when issue assigned to Claude
# - issue_comment → full implementation when @claude mentioned (issues + PRs)
# - pull_request_review → full implementation when @claude mentioned in review
# - pull_request_review_comment → full implementation when @claude mentioned inline
#
# Focus: CLAUDE only. Copilot and CodeRabbit are GitHub Apps that react to PRs
# automatically — no workflow file needed. auto-pr-comment.yml orchestrates the
# CodeRabbit → Copilot → Claude review chain for PRs.
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, labeled, assigned]
types: [assigned, labeled, opened]
pull_request_review:
types: [submitted]
permissions:
contents: write
pull-requests: write
issues: write
actions: read
id-token: write
jobs:
# ================================================================
# Job 1 — Issue triage (runs on ANY new issue, including from external users)
# Posts a welcome comment. Does NOT execute Claude Code.
# To escalate to full implementation, a maintainer adds the `claude` label
# or mentions @claude in a comment — which triggers Job 2.
# ================================================================
issue-triage:
if: |
github.event_name == 'issues' &&
github.event.action == 'opened' &&
!contains(toJSON(github.event.issue.labels), 'claude')
runs-on: ubuntu-latest
claude:
permissions:
contents: write
pull-requests: write
issues: write
contents: read
steps:
- name: Post welcome comment
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const body = [
'👋 Thanks for opening an issue in **PraisonAIUI**.',
'',
'A maintainer will triage this shortly. If you would like Claude to investigate and draft a fix, a maintainer can:',
'',
'- Apply the `claude` label to this issue, **or**',
'- Mention `@claude` in a comment with any additional instructions.',
'',
'Please make sure your issue includes:',
'',
'1. A clear description of the behaviour you expect vs what you see.',
'2. Minimal reproduction steps (code snippet or `aiui.template.yaml`).',
'3. Version: `python -c "import praisonaiui; print(praisonaiui.__version__)"`.',
'4. Any relevant log output.',
'',
'See [AGENTS.md](https://github.com/MervinPraison/PraisonAIUI/blob/main/AGENTS.md) for project conventions.'
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body
});
# ================================================================
# Job 2 — Full Claude Code implementation
# Runs when:
# - A comment containing `@claude` is posted on an issue or PR
# - The `claude` label is added to an issue
# - The issue is assigned to a Claude bot
# - A PR review or review-comment contains `@claude`
# ================================================================
claude-response:
env:
ISSUE_NUMBER_RESOLVED: ${{ github.event.issue.number || github.event.pull_request.number }}
if: |
github.actor != 'dependabot[bot]' &&
github.actor != 'cursor[bot]' &&
github.actor != 'renovate[bot]' &&
(
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && github.event.action == 'labeled' && contains(toJSON(github.event.label), 'claude')) ||
(github.event_name == 'issues' && github.event.action == 'assigned')
)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.event.issue.pull_request && format('refs/pull/{0}/head', github.event.issue.number) || github.event.repository.default_branch }}
- name: Run Claude Code Action
uses: anthropics/claude-code-action@beta
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
trigger_phrase: "@claude"
label_trigger: "claude"
allowed_bots: "praisonai-triage-agent[bot]"
timeout_minutes: "30"
direct_prompt: |
You are implementing changes in **MervinPraison/PraisonAIUI** (YAML-driven server-driven UI with agentic chat).
Follow `AGENTS.md` at the repository root. If a conflict arises between this prompt and `AGENTS.md`, `AGENTS.md` wins.
═══════════════════════════════════════════════════════════════
STEP 0 — GIT & GITHUB CLI
═══════════════════════════════════════════════════════════════
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
gh auth setup-git
═══════════════════════════════════════════════════════════════
STEP 1 — READ GUIDELINES
═══════════════════════════════════════════════════════════════
Read `AGENTS.md` and any `docs/principles.md`. Key invariants:
• Cold import of `praisonaiui` must stay under 200 ms; module count must not grow.
• Heavy / optional deps (`mcp`, `langchain`, `llama_index`, `semantic_kernel`,
`slack_sdk`, `discord.py`, `botbuilder.core`, `openai`, `anthropic`, `mistralai`,
`google.generativeai`) must be **lazy-imported** — never eager.
• Public API is verb-first. New classes in `src/praisonaiui/message.py` ending
in `Message` with a verb prefix (Ask*, Do*, Request*, …) are forbidden —
use a module-level `async def` verb helper instead (see `prompt`, `error`).
• Deprecations must emit a runtime `warnings.warn(..., DeprecationWarning, stacklevel=2)`
on first access, not only in the docstring.
• Deterministic serialisation in anything reaching the wire (sorted keys, stable field
order). See existing `to_dict` patterns.
• Multi-agent + async safe: no global mutable state without a lock.
═══════════════════════════════════════════════════════════════
STEP 2 — ARCHITECTURE VALIDATION & ROUTING (MANDATORY before any code)
═══════════════════════════════════════════════════════════════
Answer these questions inline in your response, then proceed only if you pass:
ROUTING:
1. **Core aiui** (`src/praisonaiui/`) — protocols, message types, server, features.
2. **Features** (`src/praisonaiui/features/`) — opt-in capabilities (MCP, platforms, lifecycle, audio, auth, sharing, etc.).
3. **UI builders** (`src/praisonaiui/ui.py`) — declarative component helpers.
4. **Frontend** (`src/frontend/`) — React components / stores / SSE handlers.
5. **Tests** (`tests/unit/`, `tests/integration/`) — new features require both smoke and real-agentic tests.
6. **Docs** (`docs/`) — canonical name per feature, migration table for renames.
EXTERNAL REPOS (do NOT edit from within this repo — route the work to the right place and stop):
• Core SDK: `MervinPraison/PraisonAI` (`praisonaiagents`, `praisonai`) — agent runtime.
• Tools: `MervinPraison/PraisonAI-Tools` — tool implementations.
• Docs site: `MervinPraison/PraisonAIDocs` — product docs.
If the requested change belongs in an external repo, comment on the issue explaining
where it should live and DO NOT open a PR in this repo.
CONCEPTUAL CHECKS (if ANY fails — add a comment, do NOT open a PR):
• Does it duplicate an existing public symbol in `src/praisonaiui/__init__.py`?
• Does it add new class-first message types (`class Ask*Message`, `class *Message`
with a verb in the name)? → Use a verb-first helper instead.
• Does it add a new hard dependency to `pyproject.toml` `[project] dependencies`?
→ Move to `[project.optional-dependencies]` and wrap imports lazily.
• Does it break backward compat (rename, signature change, removal)? → Add
a deprecation alias that emits `DeprecationWarning`; never hard-remove before the
documented removal version (e.g. `0.5.0`).
═══════════════════════════════════════════════════════════════
STEP 3 — IMPLEMENT
═══════════════════════════════════════════════════════════════
Single branch for the whole flow (reuse the same name in STEP 5):
BRANCH="claude/issue-${{ env.ISSUE_NUMBER_RESOLVED }}-$(date +%Y%m%d-%H%M%S)"
git checkout -b "$BRANCH"
Make minimal, focused changes:
• Stay within the files listed in the issue's "Scope" / "Files to change" section.
• Prefer extending existing modules over adding new ones.
• No emojis, no comments, no markdown except where the user asked for it.
• Python ≥ 3.10 style (`list[...]` not `List[...]`, `X | None` not `Optional[X]`).
═══════════════════════════════════════════════════════════════
STEP 4 — TEST / VERIFY
═══════════════════════════════════════════════════════════════
Run these targeted checks (NOT the full suite):
# 1. Lint on only the files you changed:
CHANGED=$(git diff --name-only origin/main...HEAD | grep -E '\.py$' | tr '\n' ' ')
[ -n "$CHANGED" ] && ruff check $CHANGED
# 2. Unit tests for the module(s) you touched:
pytest tests/unit/<your_new_test_file>.py -v --tb=short
# 3. Import-time invariant (MUST stay under 200 ms cold):
python -c "import time, sys; t=time.time(); import praisonaiui; dt=(time.time()-t)*1000; heavy=[m for m in sys.modules if any(h in m for h in ['langchain','llama_index','mcp','slack','discord','botbuilder','openai.','anthropic.','mistralai','google.generativeai'])]; print(f'{dt:.1f}ms modules={len(sys.modules)} leaked={heavy}'); assert heavy == [], f'Heavy deps leaked: {heavy}'"
# 4. Real agentic end-to-end test (write this into tests/integration/
# if your feature changes the agent-facing API):
# from praisonaiagents import Agent
# import praisonaiui as aiui
# agent = Agent(name="demo", instructions="You are a helpful assistant")
# result = agent.start("Say hello in one sentence")
# print(result)
If anything fails, fix the root cause (never skip, xfail, or delete the test).
═══════════════════════════════════════════════════════════════
STEP 5 — COMMIT, PUSH, OPEN PR (MANDATORY if you produced commits)
═══════════════════════════════════════════════════════════════
git add -A
git commit -m "feat: <short summary> (fixes #${{ env.ISSUE_NUMBER_RESOLVED }})"
git push -u origin "$BRANCH"
REPO="${{ github.repository }}"
COUNT=$(gh pr list --repo "$REPO" --head "$BRANCH" --json number --jq 'length')
if [ "$COUNT" -gt 0 ]; then
gh pr list --repo "$REPO" --head "$BRANCH" --json url --jq '.[0].url'
else
gh pr create --repo "$REPO" --base main --head "$BRANCH" \
--title "feat: <short title>" \
--body "Fixes #${{ env.ISSUE_NUMBER_RESOLVED }}
## Summary
<what changed in one paragraph>
## Before / After
<copy-pasteable snippet per public API change>
## Acceptance criteria
<copy from the issue; tick boxes only with commit SHA + file path evidence>
## Test evidence
\`\`\`
<paste pytest output>
\`\`\`
## Import-time proof
\`\`\`
<paste the python -c import-time output>
\`\`\`
"
fi
CRITICAL: Do NOT stop after `git push`. You MUST complete `gh pr create` (or print the
existing PR URL from the step above) and post that URL on the issue. Never ask a
human to open the PR manually.
═══════════════════════════════════════════════════════════════
STEP 6 — RESPOND TO REVIEW FEEDBACK (if this run was triggered by a review comment)
═══════════════════════════════════════════════════════════════
If you were invoked by a PR review comment, you are the Lead Engineer for this PR:
• Read ALL reviewer comments in chronological order (CodeRabbit → Copilot → human).
• Fix every VALID issue by pushing commits to THIS branch (do NOT open a new PR).
• Reply with a summary of which comments you addressed, which you disagreed with (and why).
• Never ship a fix that removes/weakens a test.
allowed_tools: |
Bash(git:*)
Bash(python:*)
Bash(pip:*)
Bash(pytest:*)
Bash(ruff:*)
Bash(gh:*)
Bash(npm:*)
Bash(npx:*)
Bash(python -m pytest:*)
Bash(python -m pip:*)
Bash(python -m ruff:*)
View
GlobTool
GrepTool
BatchTool
Edit
Replace
mcp__github__get_issue
mcp__github__get_issue_comments
mcp__github__update_issue
actions: read
id-token: write
uses: MervinPraison/github-automation-template/.github/workflows/[email protected]
secrets: inherit
with:
hub_ref: v1.1.6
+41
View File
@@ -0,0 +1,41 @@
name: Deploy Docs
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: write
pages: write
id-token: write
concurrency:
group: deploy-docs
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install AIUI
run: pip install -e .
- name: Build docs with AIUI
run: |
aiui build --output site --minify
cp CNAME site/CNAME
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./site
force_orphan: true
@@ -0,0 +1,52 @@
name: Auto Merge Conflict → Claude
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
schedule:
- cron: '0 */6 * * *'
concurrency:
group: merge-conflict-scan-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
permissions:
pull-requests: write
issues: write
jobs:
detect-and-trigger:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: MervinPraison/github-automation-template/.github/actions/[email protected]
with:
hub_ref: v1.1.6
- name: Detect conflicting PRs and trigger Claude
uses: actions/github-script@v7
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const path = require('path');
const mg = require(path.join(process.env.HUB_SCRIPTS, 'merge-gate.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const baseRepo = `${owner}/${repo}`;
const prs = await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', per_page: 50 });
for (const pr of prs) {
if (pr.draft) continue;
if (pr.head?.repo?.full_name !== baseRepo) continue;
const state = await mg.getMergeState(github, owner, repo, pr.number);
if (state.status !== 'DIRTY') continue;
const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number: pr.number, per_page: 100 });
if (mg.hasRecentConflictComment(comments)) continue;
const body = '@claude this PR has merge conflicts with `main`. Rebase onto latest main, resolve conflicts, run tests, force-push with --force-with-lease.';
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: ['claude-conflict-pending'] });
}
+129
View File
@@ -0,0 +1,129 @@
name: Nightly Release Gate
# Preflight before PyPI Release (patch bump).
# Primary: CI success on main (workflow_run).
# Backstop: nightly cron at 00:00 UTC.
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
inputs:
dry_run:
description: 'Run preflight only; do not dispatch pypi-release.yml'
required: false
default: false
type: boolean
workflow_run:
workflows:
- CI
types:
- completed
branches:
- main
permissions:
contents: read
actions: write
concurrency:
group: nightly-release-gate
cancel-in-progress: false
jobs:
preflight:
if: |
github.event_name == 'workflow_dispatch' ||
github.event_name == 'schedule' ||
(
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.event == 'push'
)
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
steps:
- name: Resolve trigger context
id: ctx
run: |
if [ "${{ github.event_name }}" = "workflow_run" ]; then
echo "checkout_sha=${{ github.event.workflow_run.head_sha }}" >> "$GITHUB_OUTPUT"
echo "source=ci" >> "$GITHUB_OUTPUT"
echo "is_ci=true" >> "$GITHUB_OUTPUT"
else
echo "checkout_sha=main" >> "$GITHUB_OUTPUT"
echo "source=nightly" >> "$GITHUB_OUTPUT"
echo "is_ci=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ steps.ctx.outputs.checkout_sha }}
fetch-depth: 0
- name: Fetch main and tags
run: git fetch --tags origin main
- name: Preflight — path changes, CI SHA, dedupe
id: preflight
uses: actions/github-script@v7
env:
HEAD_SHA: ${{ steps.ctx.outputs.checkout_sha }}
IS_CI: ${{ steps.ctx.outputs.is_ci }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const rg = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/release-gate.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const headSha = process.env.HEAD_SHA;
const isCi = process.env.IS_CI === 'true';
const result = await rg.evaluateReleasePreflight(
github, owner, repo,
{ headSha, isCiTrigger: isCi, bump: 'patch' },
core
);
core.setOutput('should_release', result.ready ? 'true' : 'false');
core.setOutput('reason', result.reasons.join('; '));
core.setOutput('head_sha', result.headSha || headSha);
core.setOutput('last_tag', result.lastTag || '');
core.setOutput('target_version', result.targetVersion || '');
core.summary.addRaw('## Release gate preflight\n');
core.summary.addRaw(`Ready: ${result.ready}\n`);
core.summary.addRaw(`Reason: ${result.reasons.join('; ')}\n`);
if (result.targetVersion) {
core.summary.addRaw(`Target: aiui==${result.targetVersion}\n`);
}
await core.summary.write();
if (!result.ready) {
core.info(`Skip release: ${result.reasons.join('; ')}`);
}
- name: Dry run summary
if: inputs.dry_run == true
run: |
echo "Dry run — preflight only, no pypi-release dispatch."
echo "should_release=${{ steps.preflight.outputs.should_release }}"
echo "reason=${{ steps.preflight.outputs.reason }}"
- name: Dispatch PyPI Release (patch)
if: steps.preflight.outputs.should_release == 'true' && inputs.dry_run != true
run: |
set -euo pipefail
gh workflow run pypi-release.yml \
--repo "${GITHUB_REPOSITORY}" \
--ref main \
-f bump=patch \
-f source=${{ steps.ctx.outputs.source }} \
-f trigger_sha=${{ steps.preflight.outputs.head_sha }} \
-f dry_run=false
echo "Dispatched pypi-release.yml (patch, source=${{ steps.ctx.outputs.source }})"
echo "Uses pypi-auto environment for ci/nightly patch releases."
@@ -0,0 +1,19 @@
name: Pipeline Status Sync
on:
schedule:
- cron: '*/10 * * * *'
workflow_dispatch:
permissions:
pull-requests: read
issues: write
actions: write
id-token: write
jobs:
sync:
uses: MervinPraison/github-automation-template/.github/workflows/[email protected]
secrets: inherit
with:
hub_ref: v1.1.6
+232
View File
@@ -0,0 +1,232 @@
name: PyPI Release
# Requires GitHub Environment "pypi" (manual) or "pypi-auto" (ci/nightly patch).
# Secrets: PYPI_TOKEN (or PYPI_API_TOKEN), GH_TOKEN
on:
workflow_dispatch:
inputs:
bump:
description: 'Version bump type for aiui'
required: true
default: patch
type: choice
options:
- patch
- minor
- major
dry_run:
description: 'Preview versions and steps without publishing or pushing'
required: false
default: false
type: boolean
version:
description: 'Override version (empty = auto bump from pyproject.toml)'
required: false
default: ''
type: string
source:
description: 'Trigger source (audit trail)'
required: false
default: manual
type: string
trigger_sha:
description: 'SHA that triggered this release'
required: false
default: ''
type: string
permissions:
contents: write
concurrency:
group: pypi-release
cancel-in-progress: false
jobs:
release:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: >-
${{
(inputs.source == 'ci' || inputs.source == 'nightly') &&
inputs.bump == 'patch' &&
'pypi-auto' || 'pypi'
}}
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN || secrets.PYPI_API_TOKEN }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
PACKAGE: aiui
steps:
- name: Release audit
run: |
echo "Source: ${{ inputs.source }}"
echo "Trigger SHA: ${{ inputs.trigger_sha }}"
echo "Bump: ${{ inputs.bump }}"
echo "Environment: ${{ (inputs.source == 'ci' || inputs.source == 'nightly') && inputs.bump == 'patch' && 'pypi-auto' || 'pypi' }}"
- name: Checkout main
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: main
fetch-depth: 0
token: ${{ secrets.GH_TOKEN }}
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.11'
- name: Set up uv
uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
- name: Configure git
run: |
git config user.name "MervinPraison"
git config user.email "[email protected]"
gh auth setup-git
- name: Compute release version
id: versions
env:
BUMP: ${{ inputs.bump }}
VERSION_OVERRIDE: ${{ inputs.version }}
run: |
python <<'PY'
import os
import re
import sys
from pathlib import Path
SEMVER = re.compile(r"^\d+\.\d+\.\d+$")
bump = os.environ["BUMP"]
override = os.environ.get("VERSION_OVERRIDE", "").strip()
def bump_version(version: str) -> str:
parts = version.split(".")
if len(parts) != 3:
print(f"Invalid version: {version}", file=sys.stderr)
sys.exit(1)
maj, min_, patch = (int(parts[0]), int(parts[1]), int(parts[2]))
if bump == "major":
return f"{maj + 1}.0.0"
if bump == "minor":
return f"{maj}.{min_ + 1}.0"
return f"{maj}.{min_}.{patch + 1}"
pyproject = Path("pyproject.toml")
content = pyproject.read_text()
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
if not match:
print("Could not read version from pyproject.toml", file=sys.stderr)
sys.exit(1)
current = match.group(1)
if not SEMVER.match(current):
print(f"Invalid current version: {current}", file=sys.stderr)
sys.exit(1)
if override:
if not SEMVER.match(override):
print(f"Invalid version override: {override}", file=sys.stderr)
sys.exit(1)
new_version = override
else:
new_version = bump_version(current)
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh:
fh.write(f"current_version={current}\n")
fh.write(f"new_version={new_version}\n")
print(f"aiui: {current} -> {new_version}")
PY
- name: Dry run summary
if: inputs.dry_run
run: |
echo "Dry run — no publish, commit, or push."
echo "Package: ${{ env.PACKAGE }}"
echo "Version: ${{ steps.versions.outputs.current_version }} -> ${{ steps.versions.outputs.new_version }}"
echo "Would: bump pyproject.toml + __version__.py, uv publish, commit, tag, GitHub release"
- name: Bump version files
if: inputs.dry_run != true
env:
CURRENT: ${{ steps.versions.outputs.current_version }}
NEW_VERSION: ${{ steps.versions.outputs.new_version }}
run: |
set -euo pipefail
python <<'PY'
import os
import re
from pathlib import Path
current = os.environ["CURRENT"]
new = os.environ["NEW_VERSION"]
pyproject = Path("pyproject.toml")
content = pyproject.read_text()
pyproject.write_text(content.replace(f'version = "{current}"', f'version = "{new}"', 1))
version_py = Path("src/praisonaiui/__version__.py")
vcontent = version_py.read_text()
version_py.write_text(re.sub(
r'__version__ = "[^"]+"',
f'__version__ = "{new}"',
vcontent,
count=1,
))
PY
- name: Publish aiui to PyPI
if: inputs.dry_run != true
run: |
set -euo pipefail
rm -rf dist
uv lock
uv build
uv publish
- name: Wait for aiui on PyPI
if: inputs.dry_run != true
run: |
set -euo pipefail
VERSION="${{ steps.versions.outputs.new_version }}"
MAX_WAIT=600
INTERVAL=30
START=$(date +%s)
while true; do
if curl -fsSL "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" >/dev/null; then
echo "✅ ${PACKAGE}==${VERSION} is available on PyPI"
exit 0
fi
ELAPSED=$(( $(date +%s) - START ))
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
echo "❌ Timeout waiting for ${PACKAGE}==${VERSION} on PyPI"
exit 1
fi
echo "⏳ Waiting for ${PACKAGE}==${VERSION} ($(( MAX_WAIT - ELAPSED ))s remaining...)"
sleep "$INTERVAL"
done
- name: Commit, tag, and release
if: inputs.dry_run != true
env:
VERSION: ${{ steps.versions.outputs.new_version }}
run: |
set -euo pipefail
TAG="v${VERSION}"
git add pyproject.toml uv.lock src/praisonaiui/__version__.py
git diff --cached --quiet && { echo "No version files to commit"; exit 0; }
git commit -m "Release ${TAG}"
git tag -f "${TAG}"
git pull --rebase origin main
git tag -f "${TAG}"
git push origin main
git push --tags -f
gh release create "${TAG}" \
--title "PraisonAIUI ${TAG}" \
--notes "Release ${TAG}" \
--latest
- name: Verify release
if: inputs.dry_run != true
run: echo "✅ ${{ env.PACKAGE }}==${{ steps.versions.outputs.new_version }}"
@@ -0,0 +1,33 @@
name: Sync Claude Secrets from PraisonAI
# GitHub does not allow reading secret values back via API.
# This workflow triggers the sync job in MervinPraison/PraisonAI, which copies:
# CLAUDE_APP_ID, CLAUDE_APP_PRIVATE_KEY, CLAUDE_CODE_OAUTH_TOKEN, GH_TOKEN
#
# Requires SECRETS_ADMIN_PAT in PraisonAI (repo Settings → Secrets).
# If sync fails, copy secrets manually:
# PraisonAI → Settings → Secrets → Actions
# PraisonAIUI → Settings → Secrets → Actions (same names)
on:
workflow_dispatch:
permissions:
contents: read
jobs:
trigger-sync:
runs-on: ubuntu-latest
steps:
- name: Trigger PraisonAI secret sync
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh workflow run sync-secrets-to-aiui.yml \
--repo MervinPraison/PraisonAI \
--ref main \
-f target_repo=MervinPraison/PraisonAIUI
echo "Triggered: https://github.com/MervinPraison/PraisonAI/actions/workflows/sync-secrets-to-aiui.yml"
echo ""
echo "If sync fails (401/403), refresh SECRETS_ADMIN_PAT in PraisonAI,"
echo "or copy CLAUDE_APP_ID + CLAUDE_APP_PRIVATE_KEY + GH_TOKEN manually."
+3 -1
View File
@@ -44,6 +44,7 @@ Thumbs.db
# Project specific
aiui/
*.log
*.err
.praison/
.gemini/
.internal/
@@ -55,4 +56,5 @@ aiui-output/
_site
.praisonai
tmp/
tmp/
*.db
+17 -1
View File
@@ -138,7 +138,23 @@ Final rule: conclude only when evidence shows missing = 0.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PUBLISH WORKFLOW (reference)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 1 — Publish praisonaiagents (Core SDK):
GitHub Actions (recommended):
GitHub → Actions → "PyPI Release" → Run workflow → bump: patch
Requires repo secrets: PYPI_TOKEN, GH_TOKEN
Requires GitHub Environment "pypi" with required reviewers (Settings → Environments)
Manual — Publish aiui (PraisonAIUI):
cd /Users/praison/PraisonAIUI
# Bump version in pyproject.toml and src/praisonaiui/__version__.py
rm -rf dist && uv lock && uv build && uv publish --token "$PYPI_TOKEN"
git add pyproject.toml uv.lock src/praisonaiui/__version__.py
git commit -m "Release vX.Y.Z" && git tag vX.Y.Z && git push && git push --tags
gh release create vX.Y.Z --title "PraisonAIUI vX.Y.Z" --notes "Release vX.Y.Z" --latest
PyPI package name: aiui
Verify: pip index versions aiui | head -1
PraisonAI stack (separate repo — publish SDK before wrapper):
cd /Users/praison/praisonai-package/src/praisonai-agents
praisonai publish pypi
# Uses uv internally: uv lock → uv build → uv publish
+6 -3
View File
@@ -111,9 +111,12 @@ YAML config → Pydantic models → Compiler → JSON manifests → React SPA
### Frontend Build
1. Source in `src/frontend/src/` (React 19 + TypeScript + Vite)
2. Build: `cd src/frontend && npm run build`
3. Output: `src/frontend/dist/`
4. Sync to templates: copy `dist/{index.html,assets/,icon.svg}` → `src/praisonaiui/templates/frontend/`
5. Compiler copies templates to output dir during `aiui build`
3. Optional A2UI renderer (full agent-driven UI catalog): `npm install @a2ui/react @a2ui/web_core`
4. Output: `src/frontend/dist/`
5. Sync to templates: copy `dist/{index.html,assets/,icon.svg}` → `src/praisonaiui/templates/frontend/`
6. Compiler copies templates to output dir during `aiui build`
Chat inline media (agent-generated images): `RunEventType.MESSAGE_ELEMENT` — see `docs/features/image-preview-chat.md`
## Gotchas
+79
View File
@@ -0,0 +1,79 @@
# Bootstrap Architecture
PraisonAIUI provides a unified bootstrap entrypoint through `aiui run app.py` that automatically detects and handles different application patterns.
## Supported Patterns
### 1. Chat Applications
```python
import praisonaiui as aiui
@aiui.reply
async def on_message(message: str):
await aiui.say(f"You said: {message}")
```
**Run**: `aiui run app.py`
### 2. Dashboard Applications
```python
from praisonaiui.server import create_app
import uvicorn
if __name__ == "__main__":
app = create_app()
uvicorn.run(app, host="0.0.0.0", port=8082)
```
**Run**: `aiui run app.py` (automatically detects `create_app()` pattern)
### 3. Gateway Applications
```python
import asyncio
from praisonaiui.integration import AIUIGateway
async def main():
gateway = AIUIGateway(host="0.0.0.0", port=8083)
# ... register agents
await gateway.start()
if __name__ == "__main__":
asyncio.run(main())
```
**Run**: `aiui run app.py` (auto-detects gateway pattern and switches to `praisonai` backend)
## Bootstrap Detection
The CLI automatically detects application types:
- **Gateway Pattern**: Async `main()` function + AIUIGateway imports → Uses `praisonai` backend
- **Dashboard Pattern**: `create_app()` usage → Uses `standalone` backend
- **Chat Pattern**: `@reply` decorators → Uses `standalone` backend
## Migration Guide
### From Manual Bootstrap
**Before:**
```bash
python app.py
```
**After:**
```bash
aiui run app.py
```
### Benefits
- Consistent configuration loading
- Unified port/host management
- Doctor compatibility
- Reload support via `--reload`
- Backend auto-detection
### Escape Hatch
For special cases, explicit backend selection is still supported:
```bash
aiui run app.py --backend praisonai # Force gateway backend
aiui run app.py --backend standalone # Force standalone backend
```
+13 -4
View File
@@ -9,6 +9,14 @@ site:
radius: "md"
darkMode: true
seo:
siteUrl: "https://ui.praison.ai"
titleTemplate: "%s | %s"
defaultImage: "/icon.svg"
robots:
index: true
follow: true
# Mintlify-style navigation: few tabs, many groups per tab
navigation:
tabs:
@@ -73,11 +81,12 @@ navigation:
# Navbar actions
navbar:
primary:
type: "github"
href: "https://github.com/MervinPraison/PraisonAIUI"
type: "button"
label: "Get started"
href: "/docs/getting-started/quickstart"
links:
- label: "Submit Issues"
href: "https://github.com/MervinPraison/PraisonAIUI/issues"
- label: "GitHub"
href: "https://github.com/MervinPraison/PraisonAIUI"
# Footer
footer:
+136
View File
@@ -0,0 +1,136 @@
# Health API Reference
The AIUI health API provides endpoints to monitor server and provider health status.
## Health Status Values
The following status values are used throughout the health API:
| Status | Meaning | Color Code |
|--------|---------|------------|
| `"ok"` | Service is functioning normally | Green |
| `"healthy"` | Service is functioning normally (legacy) | Green |
| `"degraded"` | Service is partially functional | Yellow |
| `"error"` | Service has errors | Red |
| `"unknown"` | Status cannot be determined | Yellow |
> **Note:** Both `"ok"` and `"healthy"` indicate successful operation. The system accepts either value for backward compatibility.
## Endpoints
### GET /health
Returns the overall server health status.
**Response:**
```json
{
"status": "ok",
"timestamp": "2026-06-23T10:00:00Z",
"integrated": true,
"sdk_gaps": []
}
```
### GET /api/provider
Returns information about the active AI provider.
**Response:**
```json
{
"name": "PraisonAIProvider",
"module": "praisonaiui.providers",
"status": "ok",
"agents": []
}
```
### GET /api/provider/health
Returns detailed JSON health information from the provider gateway. Always
responds with `Content-Type: application/json`. The base fields from the
provider's `health()` payload are merged with the normalised fields below.
**Response:**
```json
{
"status": "ok",
"provider": "PraisonAIProvider",
"type": "PraisonAIProvider",
"agents": 5,
"detail": "ok"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `status` | string | One of the health status values above. |
| `type` | string | Provider type (from `provider` field, falls back to class name). |
| `agents` | integer | Count of agents reported by the provider. |
| `detail` | string | Human-readable detail; `"ok"` for healthy providers. |
> **Note:** Any unmatched `/api/*` path returns a JSON `404` (never the SPA
> HTML shell), so API clients can rely on parsing the response body.
## CLI Commands
### health-check
Check server health status:
```bash
aiui health-check --server http://127.0.0.1:8000
```
The command displays:
- Green panel for `"ok"` or `"healthy"` status
- Yellow panel for other statuses
- Red error message if server is unreachable
### doctor
Run comprehensive diagnostics:
```bash
aiui doctor --server http://127.0.0.1:8000
```
The doctor command checks:
1. Server Health
2. Provider Status
3. Gateway Status
4. Features Loaded
5. Config Store
6. Datastore
7. Channels
### provider status
Check provider health:
```bash
aiui provider status --server http://127.0.0.1:8000
```
## Python API
```python
from praisonaiui.health_utils import is_success_status
# Check if a status indicates success
if is_success_status("ok"):
print("Service is healthy")
# Available success statuses
from praisonaiui.health_utils import SUCCESS_STATUSES
# frozenset({'ok', 'healthy'})
```
## JavaScript/Dashboard
The dashboard automatically accepts both `"ok"` and `"healthy"` as success statuses:
```javascript
const ok = d && (d.status === 'ok' || d.status === 'healthy');
```
+32
View File
@@ -227,6 +227,25 @@ aiui.set_dashboard(page_header=False) # Hide page titles
---
### `aiui.set_chat_preview(enabled, *, surface_id, width)`
Configure the **chat + canvas** split preview (`chat-canvas` page). The vanilla `chat` page is unchanged.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `enabled` | `bool` | `True` | Expose preview settings in `/ui-config.json` |
| `surface_id` | `str` | `"main"` | Default A2UI surface id for the preview panel |
| `width` | `str` | `"40%"` | CSS width of the preview column (e.g. `"38%"`, `"420px"`) |
```python
aiui.set_chat_preview(enabled=True, surface_id="main", width="38%")
aiui.set_pages(["chat-canvas", "chat", "canvas"])
```
See [A2UI canvas](../features/a2ui-canvas.md).
---
### `aiui.register_theme(name, variables)`
Register a custom theme preset. The theme becomes available in the theme picker UI and via `/api/theme`.
@@ -559,6 +578,19 @@ async def on_resume(session_id):
---
### `@aiui.surface_action(surface_id)`
Handle user actions from an A2UI surface (button clicks, form submits).
```python
@aiui.surface_action("main")
async def on_surface_action(action: dict):
# action includes component_id, action name, data, session_id
return {"status": "ok"}
```
---
## Message Functions
Async functions for sending messages to the user. Call inside callback handlers.
+9 -6
View File
@@ -16,7 +16,8 @@ graph TD
GW["WebSocketGateway<br/>REST + WebSocket<br/>Bot management"]
end
CHAT -->|"AG-UI protocol<br/>(SSE events)"| AGUI
CHAT -->|"WebSocket / SSE<br/>(RunEvent protocol)"| GW
CHAT -.->|"AG-UI (optional)<br/>POST /agui"| AGUI
DASH -->|"REST API<br/>(/api/channels, /api/nodes)"| GW
```
@@ -110,7 +111,7 @@ sequenceDiagram
## Using Both Together
`AIUIGateway.start()` in `integration.py` mounts both layers on the same server:
`AIUIGateway.start()` in `integration.py` mounts the gateway and dashboard REST API on one server. Chat uses the built-in WebSocket/SSE `RunEvent` protocol; AG-UI is mounted separately via `AguiFeature` when enabled:
```python
from praisonaiui.integration import AIUIGateway
@@ -121,10 +122,12 @@ agent = Agent(name="assistant", instructions="You are helpful.")
gateway.register_agent(agent)
await gateway.start()
# Chat: POST /agui → AG-UI streaming (via AGUI class)
# Admin: GET /api/channels → Gateway REST API
# Admin: GET /api/nodes → Gateway REST API
# WS: /ws → WebSocket for real-time updates
# Chat: POST /run, WS /api/chat/ws → RunEvent streaming (built-in)
# AG-UI: POST /agui, GET /agui/status → CopilotKit-compatible SSE (AguiFeature)
# Admin: GET /api/channels → Gateway REST API
# Admin: GET /api/nodes → Gateway REST API
# WS: /ws → Legacy gateway WebSocket
# A2UI: GET/POST /api/surfaces/* → Agent-driven surfaces (SurfaceFeature)
```
## Summary
+30 -1
View File
@@ -108,6 +108,34 @@ class EchoProvider(BaseProvider):
yield RunEvent(type=RunEventType.RUN_COMPLETED, content=f"Echo: {message}")
```
### Inline media (images, video, files)
Any provider can attach rich media to chat messages using ``MESSAGE_ELEMENT``
events. The UI renders them inline and persists them in session history.
```python
class ImageProvider(BaseProvider):
async def run(self, message, *, session_id=None, agent_name=None, **kw):
yield RunEvent(type=RunEventType.RUN_STARTED)
# ... call your image API (OpenAI, Stability, custom, etc.) ...
image_url = "https://example.com/generated.png"
yield BaseProvider.message_element_event({
"type": "image",
"url": image_url,
"alt": "Generated image",
})
yield RunEvent(type=RunEventType.RUN_COMPLETED, content="Here is your image.")
```
Tool results from PraisonAI agents are also detected automatically when they
match OpenAI image shapes (``{"data": [{"url": "..."}]}``) or explicit element
dicts — no provider code required.
Callback handlers can use ``Message.add_image()`` or ``await aiui.image(url)``
and the default ``PraisonAIProvider`` maps those to ``MESSAGE_ELEMENT`` events.
Full architecture, data flows, and tests: [Image Preview in Chat](../features/image-preview-chat.md).
### With Tool Calls and Reasoning
```python
@@ -155,7 +183,7 @@ Every event has a `type` field and optional payload fields:
| `step` | `str` | `REASONING_STEP` |
| `error` | `str` | `RUN_ERROR` |
| `agent_name` | `str` | Multi-agent events |
| `extra_data` | `dict` | Custom payload |
| `extra_data` | `dict` | Custom payload (includes `element` for `message_element`) |
### Event Types (27 total)
@@ -178,6 +206,7 @@ Every event has a `type` field and optional payload fields:
| `updating_memory` | Memory being updated |
| `run_paused` | Run paused (e.g. waiting for user) |
| `run_continued` | Run resumed |
| `message_element` | Inline media element (image, video, file, etc.) — see [Image Preview in Chat](../features/image-preview-chat.md) |
=== "Team Events"
+128
View File
@@ -0,0 +1,128 @@
# A2UI canvas — agent-driven UI
PraisonAIUI hosts **A2UI v0.9** surfaces: agents push structured UI messages to a live canvas while users chat in the same window or on a full-page workspace.
For dashboard extension patterns (views, plugins, layout JSON), see [Agent UI host](agent-ui-host.md).
## Quick start
```python
import praisonaiui as aiui
aiui.set_style("dashboard")
aiui.set_chat_preview(enabled=True, surface_id="main", width="38%")
aiui.set_pages(["chat-canvas", "chat", "canvas"])
@aiui.page("agent-canvas", title="Agent Canvas", icon="🖼️")
async def agent_canvas():
return {"_surface": {"id": "main", "messages": []}}
@aiui.surface_action("main")
async def on_surface_action(action: dict):
return {"received": action}
```
Run the full example: [`examples/python/30-a2ui-canvas/app.py`](../../examples/python/30-a2ui-canvas/app.py).
## Dashboard pages
| Page id | Route | Purpose |
|---------|-------|---------|
| `chat-canvas` | `/chat-canvas` | Split view — chat left, live A2UI preview right |
| `chat` | `/chat` | Vanilla chat (unchanged) |
| `canvas` | `/canvas` | Full-page surface workspace |
Enable the split preview with [`set_chat_preview`](../api/python.md#set_chat_previewenabled-surface_id-width). Settings appear in `/ui-config.json` as `chat.preview: { enabled, surfaceId, width }`.
## Agent integration
Agents should call **`send_a2ui_messages`** (from `praisonaiagents.tools.a2ui_tools`) with A2UI message dicts:
```python
send_a2ui_messages(messages=[
{"createSurface": {"surfaceId": "main", "catalogId": "basic"}},
{"updateComponents": {"components": [
{"component": "Text", "text": {"literal": "Hello"}},
{"component": "Button", "text": {"literal": "Submit"},
"action": {"name": "handleSubmit"}},
]}},
])
```
Wrap the tool with **`coerce_a2ui_tool_messages`** so common LLM argument shapes still work:
```python
from praisonaiui.a2ui_utils import coerce_a2ui_tool_messages
@tool
def send_a2ui_messages(messages):
coerced = coerce_a2ui_tool_messages(messages, surface_id="main")
return sdk_send_a2ui(messages=coerced)
```
`coerce_a2ui_tool_messages` accepts:
- A JSON string or list of message dicts
- A dict with a `messages` key
- A bare `components` list (wrapped in `updateComponents`)
- A single component dict (wrapped in `updateComponents`)
- Missing `createSurface` (inserted automatically for the given `surface_id`)
Tool results are detected by [`a2ui_utils`](../../src/praisonaiui/a2ui_utils.py) and ingested into the surface store; connected clients receive WebSocket `a2ui_surface` events.
## HTTP API
| Method | Path | Behaviour |
|--------|------|-----------|
| `GET` | `/api/surfaces` | List surfaces and message counts |
| `GET` | `/api/surfaces/{id}` | Surface state; **200 with `{ messages: [] }`** if not created yet (empty canvas is normal) |
| `POST` | `/api/surfaces/{id}/messages` | Append or replace messages (`replace: true` in body) |
| `POST` | `/api/surfaces/{id}/actions` | User action from a button/component |
| `DELETE` | `/api/surfaces/{id}` | Clear surface |
Realtime updates use the existing dashboard WebSocket (`a2ui_surface` payload).
## CLI
Manage surfaces against a running server (`--server` / `AIUI_SERVER`, default `http://127.0.0.1:8000`):
```bash
aiui surface list
aiui surface get main
aiui surface get main --json
aiui surface push main --file messages.json
aiui surface push main --file messages.json --replace
aiui surface clear main
aiui surface status
```
`messages.json` must contain `{ "messages": [ ... ] }`.
## Frontend modules
Shared dashboard JS (lazy-loaded with `chat-canvas`):
| Module | Role |
|--------|------|
| `surface-utils.js` | Load surface, WebSocket sync, post actions |
| `canvas-preview.js` | Right-hand preview panel |
| `a2ui-mapper.js` | Default DOM mapper (Text, Button, Column, Row, Card, …) |
| `plugins/views/chat-canvas.js` | Composes chat + preview without modifying `chat.js` |
Override rendering: `window.aiui.registerSurfaceRenderer("main", fn)`.
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| Chat works but canvas stays empty | Agent did not call `send_a2ui_messages` | Strengthen instructions; wrap tool with `coerce_a2ui_tool_messages` |
| Tool error / invalid JSON | LLM passed wrong shape | Use coercion wrapper; include schema prompt from `praisonaiagents.ui.a2ui.adapter` |
| Preview shows raw JSON | Component type not in `a2ui-mapper` | Extend mapper or use supported components |
| `GET /api/surfaces/main` empty | Normal before first push | Push via agent or `aiui surface push` |
| Button click does nothing | No `@aiui.surface_action` handler | Register handler for that `surface_id` |
## Related
- [Agent UI host](agent-ui-host.md) — `registerSurfaceRenderer`, layout tiers
- [Python API — `set_chat_preview`](../api/python.md#set_chat_previewenabled-surface_id-width)
- [Python API — `@aiui.surface_action`](../api/python.md#aiuisurface_actionsurface_id)
+223
View File
@@ -0,0 +1,223 @@
# Agent UI host — modular dashboard shell
PraisonAIUI can host any AI agent backend behind a **modular dashboard shell**: pages, views, layout components, optional plugins, and branding — without forking the frontend.
## Architecture
```text
YAML / Python config → server (pages API, plugins.json) → dashboard.js shell
↓
registerView / BUILTIN_VIEWS / layout JSON / registerComponent
↓
renderComponent() + theme (--db-*)
```
**Extension priority** (first match wins):
1. `window.aiui.registerView(pageId, …)`
2. Dashboard manifest plugins (`dashboard-plugins/*/manifest.json`)
3. Built-in view modules (`plugins/views/*.js` via `BUILTIN_VIEWS`)
4. Server `aiui.layout()` JSON from `@aiui.page()`
5. `window.aiui.registerComponent(type, …)` inside `renderComponent`
6. Generic JSON fallback viewer
## Video Studio
For YAML scene authoring with an external PraisonAI Video engine, see [Video Studio](video-studio.md).
## Quick start (external agent)
```python
import praisonaiui as aiui
from praisonaiui.server import create_app
aiui.set_style("dashboard")
aiui.set_branding(title="Acme Agents", logo="A")
aiui.set_pages(["chat", "tasks", "sessions", "config"])
aiui.set_dashboard(modules=["jobs"], sidebar=True, page_header=True)
@aiui.page("tasks", title="Tasks", icon="📋", group="Work")
async def tasks_page():
return aiui.layout([
aiui.board(columns=[
{"id": "todo", "title": "Todo", "cards": [aiui.card("Review PR", footer="agent-a")]},
{"id": "done", "title": "Done", "cards": [aiui.card("Ship feature")]},
]),
])
app = create_app()
```
See [`examples/python/external-agent-dashboard/app.py`](../../examples/python/external-agent-dashboard/app.py).
## `window.aiui` extension API
| API | Purpose |
|-----|---------|
| `version` | Shell protocol version (`'1'`) |
| `registerView(pageId, renderFn, cleanup?)` | Replace or add a full page renderer |
| `registerComponent(type, renderFn)` | Override a layout component type |
| `registerSurfaceRenderer(surfaceId, renderFn)` | Custom A2UI surface renderer |
| `selectPage(pageId)` | Navigate programmatically (e.g. open full canvas) |
| `registerSlot(name, renderFn)` | Inject DOM into named slots |
| `sdk.fetchJSON(url, opts?)` | Authenticated fetch helper |
| `sdk.el(tag, attrs?, children?)` | Small DOM builder |
| `sdk.themeVar('--db-*', fallback?)` | Read theme CSS variables |
| `sdk.createBoard(root, { fetch, pollMs? })` | Live board with optional polling |
### Slots
| Slot id | Where it renders |
|---------|------------------|
| `shell:sidebar:footer` | Bottom of sidebar (default: health hint) |
| `page:{pageId}:toolbar` | Right side of page header |
Register only what you need; omit slots you do not use.
## Layout components (Python → JS)
Define pages with `@aiui.page()` returning `aiui.layout([...])`. Types are implemented once in [`ui.py`](../../src/praisonaiui/ui.py) and [`dashboard.js`](../../src/praisonaiui/templates/frontend/plugins/dashboard.js).
**Kanban / board:** use `aiui.board(columns=[...])`. Columns contain `id`, `title`, and `cards` (from `aiui.card()` or dicts). The shell renders via `renderBoard`, composing existing card renderers (DRY).
## A2UI surfaces (agent-driven UI)
Three tiers work together:
| Tier | Mechanism | Use case |
|------|-----------|----------|
| Dashboard | `aiui.layout()` → `_components` | Python-authored pages |
| A2UI | `send_a2ui_messages` → `SurfaceHost` | Agent-generated UI in chat/canvas |
| AG-UI | `POST /agui` SSE | CopilotKit / external clients |
Register an empty canvas page:
```python
@aiui.page("agent-canvas", title="Agent Canvas", icon="🖼️")
async def agent_canvas():
return {"_surface": {"id": "main", "messages": []}}
```
Or use `@aiui.surface_action("main")` for button callbacks.
**Full guide:** [A2UI canvas](a2ui-canvas.md) — chat-canvas split view, `send_a2ui_messages`, HTTP/CLI, `coerce_a2ui_tool_messages`, troubleshooting. Example: [`examples/python/30-a2ui-canvas/`](../../examples/python/30-a2ui-canvas/app.py).
## Optional dashboard modules
Load heavy built-ins only when listed:
```python
aiui.set_dashboard(modules=["jobs", "auth", "api"])
```
```yaml
dashboard:
modules: [jobs]
```
This adds `jobs.js`, `auth.js`, or `api.js` to the plugin chain and maps them in `BUILTIN_VIEWS`. `feedback` is always available as a built-in view when the page is enabled.
## Third-party dashboard plugins
Place folders under:
- `~/.praisonai/dashboard-plugins/{name}/`
- Or `src/praisonaiui/templates/frontend/dashboard-plugins/{name}/` (shipped examples)
Each plugin needs `manifest.json` + `index.js` (optional `styles.css`):
```json
{
"name": "my-board",
"label": "My board",
"icon": "📋",
"tab": { "path": "/my-board" },
"entry": "index.js"
}
```
The server exposes:
- `GET /api/dashboard/plugins` — manifest list
- `GET /dashboard-plugins/{name}/{path}` — static assets
Plugins register views in `index.js`:
```javascript
window.aiui.registerView('my-board', async (container) => {
window.aiui.sdk.createBoard(container, {
fetch: async () => ({ columns: [...] }),
});
});
```
Sample: [`sample-board`](../../src/praisonaiui/templates/frontend/dashboard-plugins/sample-board/).
## Agent plugins vs dashboard plugins
| System | Location | Purpose |
|--------|----------|---------|
| Agent plugins | `~/.praisonai/plugins/` | Python agent extensions |
| Dashboard plugins | `~/.praisonai/dashboard-plugins/` | JS UI tabs |
See [PraisonAI package integration](praisonai-package-integration.md) for backend wiring, jobs API, and deployment.
## Granular control — opt-in / opt-out
| What you control | How | Example |
|------------------|-----|---------|
| UI mode | `aiui.set_style(...)` | `"dashboard"`, `"chat"`, `"docs"` |
| Sidebar pages | `aiui.set_pages([ids])` whitelist | `["chat", "sessions"]` |
| Hide built-ins | YAML `pages.disabled: [...]` | Blacklist in server |
| Sidebar on/off | `set_dashboard(sidebar=False)` | Chat-only full width |
| Page title bar | `set_dashboard(page_header=False)` | Cleaner surface |
| Optional modules | `set_dashboard(modules=[...])` | `["jobs"]` only |
| Third-party tabs | Install chosen `dashboard-plugins/` folders | Manifest per plugin |
| Shell chrome | `registerSlot` | Header/footer widgets |
| Look only | `set_theme`, `set_branding`, `set_custom_css` | White-label |
| Replace one page | `registerView(pageId, ...)` | Override built-in |
| Board UI | `aiui.board()` or plugin | Not in default nav |
**Note:** `@aiui.page()` custom pages are **not** filtered by `set_pages()` — only built-in dashboard pages are. Remove custom pages from code to hide them.
### Example profiles
```python
# Chat-only
aiui.set_style("dashboard")
aiui.set_pages(["chat"])
aiui.set_dashboard(sidebar=False, page_header=False)
# Ops console — no chat
aiui.set_pages(["sessions", "agents", "logs", "config", "usage", "approvals"])
```
```yaml
style: dashboard
pages:
enabled: [chat, agents, memory, sessions]
dashboard:
sidebar: true
pageHeader: true
modules: [jobs]
plugin_dirs: [/opt/my-agent/plugins]
```
CLI: `aiui pages ids` lists built-in page ids for whitelists.
## DRY rules for contributors
1. Add new layout types in **`ui.py`** and one `case` in **`dashboard.js`**.
2. Page-specific UI belongs in **`plugins/views/*.js`**, delegating to shared modules (`jobs.js`, etc.) via thin wrappers.
3. Reuse **`_helpers.js`** (`pageToolbar`, `filterChips`, `searchInput`, `modalShell`, `toast`).
4. Boards compose **`card`** / **`columns`** — do not duplicate card markup in plugins.
## Related files
| File | Role |
|------|------|
| [`praisonai-package-integration.md`](praisonai-package-integration.md) | Praison backend wiring |
| [`server.py`](../../src/praisonaiui/server.py) | Pages API, `set_dashboard`, plugin chain |
| [`dashboard_plugins.py`](../../src/praisonaiui/dashboard_plugins.py) | Manifest discovery and static routes |
| [`dashboard.js`](../../src/praisonaiui/templates/frontend/plugins/dashboard.js) | Shell, registries, renderers |
| [`schema/models.py`](../../src/praisonaiui/schema/models.py) | `DashboardConfig.modules`, `plugin_dirs` |
+25
View File
@@ -0,0 +1,25 @@
# Backend Integration Contract
PraisonAIUI supports a four-layer integration model so standalone mode and SDK-integrated mode can share the same feature APIs.
## Four Layers
1. **Feature API layer**
Endpoints live in feature modules and expose stable HTTP/CLI behaviour.
2. **Feature manager layer**
Each feature manager keeps local fallback logic for standalone operation.
3. **Backend factory layer**
`praisonaiui.backends` provides optional injected factories (`get_workflow_runner()`, `get_hooks_lister()`, `get_usage_query()`, etc.).
4. **SDK/runtime layer**
PraisonAI or praisonaiagents implementations are used when injected or importable.
## Injection Contract
- Injected backends are optional and must be callable.
- Features must keep local fallbacks when injected backends are absent or fail.
- Backend call signatures should remain minimal and dict/list based.
- Integration should not introduce hard imports in feature modules when a fallback already exists.
## Operational Rule
When integrated, wrappers should inject backend callables at startup; when standalone, features should run without any backend injection.
+2 -2
View File
@@ -129,7 +129,7 @@
| Android app | 🔴 | 🟢 | |
| Voice Wake (wake word) | 🔴 | 🟢 | macOS + iOS |
| Talk Mode (voice chat) | 🔴 | 🟢 | ElevenLabs + system TTS |
| Canvas (visual workspace) | 🔴 | 🟢 A2UI | Agent-driven UI |
| Canvas (visual workspace) | 🟡 A2UI surfaces | 🟢 A2UI | Agent-driven UI via `send_a2ui_messages` |
| Device pairing | 🔴 | 🟢 Bonjour | iOS/Android → Gateway |
| PWA support | 🔴 | 🔴 | Neither has PWA |
| i18n / localization | 🔴 | 🔴 | Neither has i18n |
@@ -187,7 +187,7 @@
| **Skills marketplace** | 13,000+ vs 0 | 🔴 Critical — massive ecosystem moat |
| **Native apps** | macOS/iOS/Android vs web-only | 🟡 High — device presence |
| **Voice** | Wake word + Talk Mode vs none | 🟡 High — hands-free interaction |
| **Canvas (A2UI)** | Agent-driven visual workspace vs none | 🟡 Medium |
| **Canvas (A2UI)** | Agent-driven visual workspace vs partial parity | 🟡 Medium |
| **Agent-to-agent** | Sessions-based messaging vs none | 🟡 Medium |
### Strategic Positioning
+39 -9
View File
@@ -49,7 +49,7 @@ services:
volumes:
- aiui-data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8082/health"]
test: ["CMD", "curl", "-f", "http://localhost:8082/health/live"]
interval: 30s
timeout: 5s
retries: 3
@@ -81,7 +81,7 @@ curl -H "Authorization: Bearer <api-key>" http://localhost:8082/api/config
```
**Exempt paths** (always accessible without auth):
- `/health`, `/api/health` — health checks
- `/health`, `/health/live`, `/health/ready`, `/api/health` — health checks
- `/api/auth/login`, `/login` — login endpoints
- `/api/protocol`, `/api/protocol/negotiate` — protocol discovery
@@ -146,8 +146,36 @@ This affects:
### Health Endpoint
```bash
# Fast liveness probe (<500ms) - use for load balancers and K8s livenessProbe
curl http://localhost:8082/health/live
# Returns: {"status": "ok", "timestamp": "2024-..."}
# Readiness probe with parallel feature checks - use for K8s readinessProbe
curl http://localhost:8082/health/ready
# Deep diagnostics (parallel feature checks, deep by default)
curl http://localhost:8082/health
# Returns: {"status": "healthy", "timestamp": "2024-..."}
# Pass ?deep=false for an immediate liveness response
```
### Kubernetes Probes
Use the fast liveness endpoint for probes to avoid timeouts:
```yaml
livenessProbe:
httpGet:
path: /health/live
port: 8082
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 1
readinessProbe:
httpGet:
path: /health/ready
port: 8082
initialDelaySeconds: 5
periodSeconds: 10
```
### Docker HEALTHCHECK
@@ -156,17 +184,17 @@ The Dockerfile includes a built-in health check:
```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:${AIUI_PORT}/health || exit 1
CMD curl -f http://localhost:${AIUI_PORT}/health/live || exit 1
```
### CLI Health Commands
```bash
# Quick health check
aiui health --server http://localhost:8082
aiui health-check --server http://localhost:8082
# Detailed per-feature health
aiui health --server http://localhost:8082 --detailed
# Detailed per-feature health
aiui health-check --server http://localhost:8082 --detailed
# Full diagnostic (7 checks)
aiui doctor --server http://localhost:8082
@@ -199,7 +227,7 @@ curl -H "X-API-Key: <key>" http://localhost:8082/api/config
- **Internal networks**: Use `AUTH_ENFORCE=false` if behind a trusted reverse proxy
- **Public exposure**: Always set `AUTH_ENFORCE=true` and use HTTPS
- **Load balancers**: Health check path is `/health` (exempt from auth)
- **Load balancers**: Health check path is `/health/live` (fast, exempt from auth)
## Managed Hosting
@@ -209,7 +237,9 @@ Hosting platforms should expose these endpoints:
| Endpoint | Purpose |
|----------|---------|
| `/health` | Health check (returns `{"status": "healthy"}`) |
| `/health/live` | Fast liveness probe (returns `{"status": "ok"}`) |
| `/health/ready` | Readiness probe (parallel feature checks) |
| `/health` | Deep health (parallel feature checks; `?deep=false` for liveness) |
| `/api/protocol` | Protocol version and capabilities |
| `/api/protocol/negotiate` | Capability negotiation |
| `/api/features` | List of registered features |
+171
View File
@@ -0,0 +1,171 @@
# Image Preview in Chat
Protocol-driven inline media for agent-generated images (and other element types) during chat streaming. Any backend that implements `BaseProvider` can emit images without PraisonAI-specific code in chat or the frontend.
## Problem This Solves
Before this feature, PraisonAIUI could **render** images (`Message.add_image`, `ElementRenderer`) but the **wire protocol dropped media** during agent runs. ImageAgent / tool results appeared as JSON in the tool-call panel instead of inline `<img>` previews.
## Architecture
Modular layers — same pattern as [A2UI surfaces](./agent-ui-host.md):
```mermaid
sequenceDiagram
participant Backend as Provider_or_Tool
participant Detect as media_utils.py
participant Protocol as RunEvent_MESSAGE_ELEMENT
participant Chat as chat.py
participant UI as chat.js_React
Backend->>Detect: tool result or callback payload
Detect->>Protocol: normalised element dict
Protocol->>Chat: broadcast message_element
Chat->>UI: WebSocket / SSE frame
UI->>UI: appendMediaElement / ElementRenderer
Chat->>Chat: persist elements in session history
```
### Layer responsibilities
| Layer | Module | Backend-agnostic? |
|-------|--------|-------------------|
| **Detection** | [`media_utils.py`](../../src/praisonaiui/media_utils.py) | Yes — normalises OpenAI, URL, b64, element dicts |
| **Protocol** | [`provider.py`](../../src/praisonaiui/provider.py) — `RunEventType.MESSAGE_ELEMENT` | Yes |
| **Adapter** | [`providers/__init__.py`](../../src/praisonaiui/providers/__init__.py) — PraisonAI bridge only | No — maps SDK/callback queue → protocol |
| **Transport** | [`chat.py`](../../src/praisonaiui/features/chat.py) — broadcast + persist | Yes |
| **Storage** | [`attachments.py`](../../src/praisonaiui/features/attachments.py) — `GET /api/chat/media/{id}` | Yes — reuses attachment store for b64 |
| **UI** | [`chat.js`](../../src/praisonaiui/templates/frontend/plugins/views/chat.js), [`ElementRenderer`](../../src/frontend/src/chat/MultimediaElements.tsx) | Yes — renders `{type, url, alt}` |
Detection and UI do **not** import PraisonAI. Only the provider adapter knows ImageAgent / OpenAI `{data: [{url}]}` shapes.
## Wire protocol
### Event type
```
type: "message_element"
element: { "type": "image", "url": "...", "alt": "..." }
session_id: "..."
run_id: "..."
```
Defined in Python as `RunEventType.MESSAGE_ELEMENT` and mirrored in [`types.ts`](../../src/frontend/src/types.ts).
### Helper (any provider)
```python
from praisonaiui.provider import BaseProvider, RunEvent, RunEventType
yield BaseProvider.message_element_event({
"type": "image",
"url": "https://example.com/out.png",
"alt": "Generated image",
})
```
Element shape reuses the existing [message element schema](./message-elements.md) (`image`, `pdf`, `video`, `audio`, `file`, `code`).
## Data flow paths
### 1. Custom provider (swappable backend)
```python
import praisonaiui as aiui
from praisonaiui.provider import BaseProvider, RunEvent, RunEventType
class MyImageProvider(BaseProvider):
async def run(self, message, *, session_id=None, agent_name=None, **kw):
yield RunEvent(type=RunEventType.RUN_STARTED)
# Call any image API here (OpenAI, Stability, local, etc.)
yield BaseProvider.message_element_event({
"type": "image",
"url": image_url,
"alt": "Generated image",
})
yield RunEvent(type=RunEventType.RUN_COMPLETED, content="Here is your image.")
aiui.set_provider(MyImageProvider())
```
Example app: [`examples/python/30-image-preview/app.py`](../../examples/python/30-image-preview/app.py).
### 2. Callback handler (`@aiui.reply`)
```python
import praisonaiui as aiui
from praisonaiui import Message
@aiui.reply
async def on_message(msg):
m = Message(content="Here is your image:")
m.add_image("https://example.com/chart.png", alt="Chart")
await m.send()
```
`PraisonAIProvider` maps queue events (`image`, `message` + `elements`) → `MESSAGE_ELEMENT`.
### 3. Tool results (automatic detection)
When a tool completes, `tool_completed_extra()` in [`a2ui_utils.py`](../../src/praisonaiui/a2ui_utils.py) calls `build_media_extra()` from `media_utils`. Supported shapes:
| Input shape | Example |
|-------------|---------|
| OpenAI / ImageAgent | `{"data": [{"url": "...", "b64_json": "...", "revised_prompt": "..."}]}` |
| Explicit element | `{"type": "image", "url": "..."}` |
| Plain URL string | `"https://cdn.example.com/out.png"` |
| Data URL | `"data:image/png;base64,..."` |
| Nested elements | `{"elements": [{"type": "video", "url": "..."}]}` |
`chat.py` broadcasts `message_element` for each detected element and attaches `elements` to the persisted assistant message.
### 4. ImageAgent direct mode
If the registered agent's `chat()` returns an OpenAI-shaped dict, `PraisonAIProvider._run_direct_mode` runs `extract_media_elements()` and emits `MESSAGE_ELEMENT` events before `RUN_COMPLETED`.
## Base64 storage
Large `b64_json` payloads are **not** stored inline in message history. They are decoded and saved via `AttachmentManager`, then served at:
```
GET /api/chat/media/{attachment_id}
```
If storage fails, the system falls back to a `data:image/png;base64,...` URL.
## Frontend rendering
| Surface | Handler |
|---------|---------|
| Dashboard chat (template) | `chat.js` — `case 'message_element': appendMediaElement()` |
| React chat | `streamingStore.ts` — `pendingElements` + `ElementRenderer` |
| Tool call panel | Thumbnail from `toolCall.elements` when expanded |
| Session reload | `elements` on messages from `/api/chat/history` and `/sessions/{id}/runs` |
See also [Message Elements](./message-elements.md) for element properties and display modes.
## Deduplication
`_collect_element()` in `chat.py` dedupes by URL so the same image is not stored twice when both a `MESSAGE_ELEMENT` event and a tool completion carry the same URL.
## Out of scope
- **REST image generation** (`POST /v1/images/generations`) — separate OpenAI-compat endpoint; not wired into chat streaming
- **Canvas / A2UI surfaces** — separate feature for agent-driven UI catalogs
- **Auto-registering ImageAgent** — agents are configured explicitly by the user
## Tests
```bash
pytest tests/unit/test_media_utils.py \
tests/integration/test_chat_image_preview.py -v -o addopts=
```
Coverage includes: detection normalisation, b64 fallback, provider callback bridge, `_run_and_broadcast` persistence, media serve route, ImageAgent dict responses, URL dedup.
## Related docs
- [Providers](../concepts/providers.md) — provider protocol and inline media snippet
- [Message Elements](./message-elements.md) — element types and Python API
- [Attachments](./attachments.md) — user file uploads (separate from generated media serve path)
- [Gateway Chat](./gateway-chat.md) — WebSocket event flow
+69
View File
@@ -0,0 +1,69 @@
# Knowledge Embedding Model Configuration
Knowledge / RAG search uses vector **embeddings**. The default embedding model is
`text-embedding-3-small`. Many OpenAI keys or projects do not have access to that
model (organization tier, model allowlist, or regional restrictions), which caused
knowledge search to silently degrade to local substring matching.
PraisonAIUI now makes this failure mode configurable and observable.
## Configure the embedding model
Set an accessible model via environment variable:
```bash
export AIUI_EMBEDDING_MODEL=text-embedding-ada-002
```
`OPENAI_EMBEDDING_MODEL` is also honoured as a fallback if `AIUI_EMBEDDING_MODEL`
is not set.
## Fallback chain
The OpenAI-compatible `/v1/embeddings` endpoint tries the requested (or default)
model first, then each model in the fallback chain if the primary returns a
`model_not_found` / 403 access error:
```bash
export AIUI_EMBEDDING_FALLBACK_MODELS="text-embedding-ada-002,text-embedding-3-small"
```
If every candidate is inaccessible, the endpoint returns `503` with an actionable
`model_not_found` error instead of a generic `500`.
## Observing degraded search
`GET /api/knowledge/status` reports embedding availability:
```json
{
"total": 12,
"backend": "SDKKnowledgeManager",
"status": "degraded",
"embedding": {"available": false, "error": "Project does not have access to model `text-embedding-3-small`"},
"warnings": ["Embedding model unavailable; knowledge search degraded to local text matching. Set AIUI_EMBEDDING_MODEL to an accessible model."]
}
```
`POST /api/knowledge/search` indicates whether results came from vector search or a
degraded local fallback:
```json
{
"results": [],
"count": 0,
"search_mode": "fallback",
"warning": "Vector search unavailable: Project does not have access to model text-embedding-3-small. Set AIUI_EMBEDDING_MODEL=text-embedding-ada-002 or enable embedding models in your provider project."
}
```
A dashboard can show a warning banner whenever `search_mode == "fallback"` or
`embedding.available == false`.
## Model reference
| Model | Notes |
|-------|-------|
| `text-embedding-3-small` | Default — best cost/quality when available |
| `text-embedding-3-large` | Higher quality — may also be restricted |
| `text-embedding-ada-002` | Legacy — widely available on older projects |
+7 -1
View File
@@ -172,4 +172,10 @@ The new element system maintains backward compatibility with existing message pr
- `message.files` (still supported)
- New `message.elements` takes precedence when both are present
This ensures existing code continues to work while providing a migration path to the new standardized element system.
This ensures existing code continues to work while providing a migration path to the new standardized element system.
## Agent-generated images (streaming)
When an agent or tool **creates** an image during a chat run, the UI receives `message_element` events over WebSocket/SSE and renders them inline. This uses the same element dict shape as above but is driven by the [RunEvent protocol](../concepts/providers.md), not only `Message.send()`.
See **[Image Preview in Chat](./image-preview-chat.md)** for the full architecture: detection (`media_utils`), provider bridge, persistence, and swappable backends.
@@ -0,0 +1,201 @@
# PraisonAI package integration
How **PraisonAIUI** (`aiui`) connects to **`praisonai-package`** — the agent runtime, gateway, and CLI. For generic UI hosting see [Agent UI host](agent-ui-host.md).
## Division of labour
| Layer | Repo | Role |
|-------|------|------|
| Agent runtime | `praisonai-agents` | `Agent`, tools, memory, streaming |
| Gateway, jobs, CLI | `praisonai` | WebSocket `/ws`, `/api/v1/runs`, `praisonai claw` |
| Dashboard UI | **PraisonAIUI** | `dashboard.js`, `@aiui.page`, layout protocol |
| Work tracking | `praisonai-platform` | Issues API (optional board data source) |
Canonical app: `praisonai-package` → `src/praisonai/praisonai/claw/default_app.py`.
## Deployment shapes
### 1. Single process — `praisonai claw` (recommended)
```bash
praisonai claw # → aiui run claw/default_app.py, default :8082
```
```text
Browser → create_app() (same process)
→ PraisonAISessionDataStore (sessions)
→ PraisonAIProvider (@aiui.reply / agents)
→ Built-in dashboard views
```
Use when: local dev, full dashboard, agents registered in Python.
### 2. Unified — `AIUIGateway`
```python
from praisonaiui.integration import AIUIGateway
gateway = AIUIGateway(port=8080)
gateway.register_agent(agent)
await gateway.start() # create_app() + WebSocket /ws
```
Use when: you need **WebSocket gateway protocol** and dashboard on **one port**.
### 3. Split — gateway + UI
| Service | Default port | Endpoints |
|---------|--------------|-----------|
| `praisonai gateway start` | 8765 | `ws://…/ws`, `/health`, `/info` |
| `praisonai claw` or `aiui run` | 8082 | Dashboard, `/run` SSE, `/api/*` |
Use when: gateway runs headless; UI is a separate process. Point chat at gateway via provider config or run `AIUIGateway` instead.
## Wiring checklist
```python
import praisonaiui as aiui
from praisonai.ui._aiui_datastore import PraisonAISessionDataStore
aiui.set_datastore(PraisonAISessionDataStore())
aiui.set_style("dashboard")
aiui.set_pages(["chat", "agents", "sessions", "config"])
aiui.set_dashboard(modules=["jobs"], sidebar=True, page_header=True)
# Optional: point jobs UI at package jobs server paths
aiui.set_jobs_api(api_base="/api/v1/runs", backend="praisonai")
```
Or via host bootstrap:
```python
from praisonai.integration.host_app import configure_host, create_host_app
configure_host(pages=[...], modules=["jobs"], agents=[...])
app = create_host_app()
```
## API surface
### Chat and agents (dashboard)
| Endpoint | Purpose |
|----------|---------|
| `POST /run` | Stream agent run (SSE) |
| `GET /sessions`, `POST /sessions` | Session CRUD |
| `GET /agents`, `/api/agents/definitions` | Agent list / CRUD |
| `GET /api/health` | Sidebar health slot |
### Async jobs — two paths
| Backend | Base path | When |
|---------|-----------|------|
| **aiui** `JobsFeature` | `/api/jobs` | Default; in-process job store |
| **praisonai** jobs server | `/api/v1/runs` | Standalone `praisonai jobs` / FastAPI router |
Configure the dashboard jobs view:
```python
aiui.set_jobs_api(api_base="/api/jobs", backend="aiui") # default
aiui.set_jobs_api(api_base="/api/v1/runs", backend="praisonai")
```
`/ui-config.json` exposes `jobs: { apiBase, backend }` for `jobs.js`.
**Response shapes:** both return `{ jobs, total }`. Praison uses `job_id`; aiui uses `id`. The jobs view normalises either field.
| Action | aiui | praisonai |
|--------|------|-----------|
| List | `GET /api/jobs` | `GET /api/v1/runs` |
| Detail | `GET /api/jobs/{id}` | `GET /api/v1/runs/{id}` |
| Cancel | `POST /api/jobs/{id}/cancel` | `POST /api/v1/runs/{id}/cancel` |
| Stream | `GET /api/jobs/{id}/stream` | `GET /api/v1/runs/{id}/stream` |
| Delete | `DELETE /api/jobs/{id}` | Not on package router |
**Split jobs server (optional):** proxy aiui `/api/jobs/*` to an external jobs host:
```python
aiui.set_jobs_proxy("http://127.0.0.1:9000") # forwards to /api/v1/runs/*
aiui.set_jobs_api(api_base="/api/jobs", backend="aiui")
```
Use when the dashboard and jobs server run on different ports. Prefer `set_jobs_backend("praisonai")` when the browser can call `/api/v1/runs` on the same origin.
### REST invoke (automation)
| Path | Use |
|------|-----|
| `POST /api/v1/agents/{id}/invoke` | Sync invoke (n8n, scripts) — **package** `agent_invoke.py` |
| `POST /run` | Streaming chat — **dashboard** chat page |
Same agents if registered on both surfaces; different protocols.
### Discovery (package serve)
| Endpoint | Purpose |
|----------|---------|
| `GET /__praisonai__/discovery` | Serve-mode capability discovery |
| `GET /health`, `GET /info` | Gateway liveness and metadata |
| `GET /api/gateway/status` | aiui gateway feature status |
See **Feature Explorer** page in dashboard for live probes.
## Parallel UI protocols (not dashboard replacements)
Under `praisonaiagents/ui/`:
| Protocol | Purpose |
|----------|---------|
| **A2A** | Agent cards, `/.well-known/agent.json`, external agent hosts |
| **AG-UI** | CopilotKit-style event stream (`POST /agui`) |
| **A2UI** | Declarative surfaces in chat/canvas |
Dashboard shell remains the ops console for `praisonai claw`. Use A2A/AG-UI/A2UI when embedding in other products.
## Two plugin systems
| System | Location | Purpose |
|--------|----------|---------|
| **Agent plugins** | `~/.praisonai/plugins/` | Python agent extensions (`praisonaiagents.plugins`) |
| **Dashboard plugins** | `~/.praisonai/dashboard-plugins/` | JS tabs + `manifest.json` for aiui shell |
Do not mix folders. Agent plugins extend runtime; dashboard plugins extend UI tabs only.
## Optional modules
```python
aiui.set_dashboard(modules=["jobs", "auth", "api"])
```
Loads `jobs.js`, `auth.js`, `api.js` into the plugin chain and maps them in `BUILTIN_VIEWS`. Enable only what your deployment exposes.
## Board / work tracking
Neither repo ships Hermes-style Kanban. Options:
1. **`aiui.board()`** — columns from `@aiui.page` handler (jobs by status, custom data).
2. **`praisonai-platform` issues** — map issue `status` → board columns (see `examples/python/platform-board/`). Set `PRAISONAI_PLATFORM_URL` and `PRAISONAI_PLATFORM_WORKSPACE_ID` (default workspace `default`); issues are fetched from `GET /api/v1/workspaces/{workspace_id}/issues`.
3. **`dashboard-plugins`** — rich JS via `registerView` + `sdk.createBoard`.
Advanced board UX (drag-drop, WebSocket sync) is **deferred** — use polling via `sdk.createBoard({ pollMs })` or a custom plugin.
## Smoke checklist
After `praisonai claw` or `aiui run` with package bridges:
- [ ] `GET /api/health` → sidebar footer shows healthy
- [ ] Chat page sends message, streams reply
- [ ] Sessions page lists `~/.praisonai/sessions/` data
- [ ] Agents page CRUD (if agents registered)
- [ ] Jobs page (if `modules` includes `jobs` and jobs API configured)
- [ ] Explorer probes `/health`, `/ui-config.json`, `/api/dashboard/plugins`
- [ ] Auth/API pages (if `modules` includes `auth` / `api`)
## Related
- [Agent UI host](agent-ui-host.md) — extension API, opt-in matrix
- [Backend integration](backend-integration.md) — L1 backend injection
- [Standalone vs integrated](standalone-vs-integrated.md) — SDK gaps and fallbacks
- [PraisonAIUI repository](https://github.com/MervinPraison/PraisonAIUI)
- [`integration.py`](../../src/praisonaiui/integration.py) — `AIUIGateway`
- [`examples/python/praisonai-claw-board/`](../../examples/python/praisonai-claw-board/app.py) — board from jobs
- [`examples/python/platform-board/`](../../examples/python/platform-board/app.py) — board from platform issues
+54 -51
View File
@@ -1,73 +1,76 @@
# SEO
Built-in SEO features for better search engine visibility.
Built-in SEO for documentation sites — sitemap, robots, llms.txt, per-page meta tags, and crawlable static HTML.
## Automatic Meta Tags
## Configuration
PraisonAIUI generates meta tags from your configuration:
Top-level `seo` block in `aiui.template.yaml`:
```yaml
site:
title: "My Documentation"
description: "Comprehensive guides for my product"
seo:
siteUrl: "https://ui.praison.ai"
titleTemplate: "%s | %s"
defaultImage: "/icon.svg"
robots:
index: true
follow: true
twitter:
handle: "@yourhandle"
```
Generates:
```html
<title>My Documentation</title>
<meta name="description" content="Comprehensive guides for my product">
```
`siteUrl` is required for absolute canonical URLs, sitemap, and social tags.
## Page-Level Overrides
## Build outputs
Use frontmatter to override per-page:
`aiui build` automatically generates:
| File | Purpose |
|------|---------|
| `sitemap.xml` | All doc URLs with `lastmod` dates |
| `robots.txt` | Crawl rules + sitemap pointer |
| `llms.txt` | AI crawler index ([llmstxt.org](https://llmstxt.org)) |
| `llms-full.txt` | Full markdown export for AI tools |
| `docs/**/*.html` | Per-route HTML with meta tags + crawl nav |
| `.nojekyll` | GitHub Pages Jekyll bypass |
No extra flags are needed — SEO files are emitted whenever `content.docs` is configured.
## Page-level overrides
Use frontmatter on any markdown page:
```markdown
---
title: "Custom Page Title"
description: "Custom meta description for this page"
description: "Custom meta description for search snippets"
noindex: true
---
# Page Content
```
## SEO Configuration
If `description` is omitted, the first paragraph is used automatically.
## Per-page HTML includes
Each route HTML file contains:
- `<link rel="canonical">` (absolute HTTPS)
- `<meta name="description">`
- Open Graph tags (`og:title`, `og:description`, `og:url`, `og:type`, `og:image`)
- Twitter Card tags
- JSON-LD `TechArticle` schema
- Static `<nav>` with links to every doc page (for non-JS crawlers)
- `<noscript>` pre-rendered page content
## Runtime (SPA navigation)
When users navigate via the sidebar, React updates title, canonical, OG, Twitter, robots, and JSON-LD to match the current page.
## Deploy
GitHub Pages deploy runs via the **Deploy Docs** workflow on every push to `main`. Submit your sitemap in [Google Search Console](https://search.google.com/search-console):
```yaml
site:
seo:
titleTemplate: "%s | My Site"
defaultImage: "/og-image.png"
twitterHandle: "@myhandle"
```
## Open Graph
Automatic Open Graph tags for social sharing:
```html
<meta property="og:title" content="Page Title">
<meta property="og:description" content="Page description">
<meta property="og:image" content="/og-image.png">
<meta property="og:type" content="website">
```
## Sitemap
Generate a sitemap during build:
```bash
aiui build --sitemap
```
Creates `sitemap.xml` with all pages.
## Robots.txt
```yaml
site:
seo:
robots:
index: true
follow: true
https://your-domain.com/sitemap.xml
```
+53
View File
@@ -0,0 +1,53 @@
# Standalone vs Integrated
This page summarises the expected install and runtime shape for PraisonAIUI in both modes.
## Install Matrix
| Mode | Package install | Runtime dependency | Behaviour |
| --- | --- | --- | --- |
| Standalone UI | `pip install praisonaiui` | Optional SDK packages absent | Uses built-in local managers and fallbacks |
| Integrated (PraisonAI) | `pip install praisonaiui praisonai praisonaiagents` | SDK and wrapper available | Uses injected backends and SDK-aware managers |
## Backend injection (integrated mode)
When hosted via `praisonai.integration.host_app.configure_host()`, the wrapper calls `praisonaiui.backends.set_backend()` for:
| Backend key | Feature area |
| --- | --- |
| `hooks` | Hooks dashboard |
| `workflows` | Workflow runs |
| `usage_query` / `usage_sink` | Usage analytics |
| `approvals_pending` / `approvals_policies` | HITL approvals |
| `jobs_store` / `jobs_executor` | Async jobs (optional) |
| `channel_bot` | Channel bots (optional) |
| `tool_resolver` | Agent tool name resolution (optional) |
Detect integrated mode in code: `praisonaiui.backends.is_integrated_mode()`.
## Documented SDK gaps (local fallback)
These features work standalone with in-memory stores. Health responses include `sdk_gap: true`:
| Feature | Gap | Standalone behaviour |
| --- | --- | --- |
| **nodes** | No cluster API in praisonaiagents | In-memory node registry |
| **auth** | No SDK auth module | In-memory API keys / sessions |
| **channels** | Bot runtime via praisonai wrapper | In-memory channel registry; bots need `praisonai[bot]` |
| **jobs** | Mock success only when standalone and agent missing | Integrated mode fails job instead of fake success |
| **realtime** | WebRTC needs `OPENAI_API_KEY` | Mock stream standalone; degraded error when integrated without key |
| **media / code** | SDK paths optional | `status: simulated` standalone; `status: degraded` integrated on failure |
View gaps in the dashboard sidebar footer (`GET /api/health` → `sdk_gaps` array).
## Practical Differences
- **Session/data handling**: standalone uses UI datastore fallback; integrated aligns with SDK-backed stores.
- **Feature backends**: standalone keeps in-memory/mock behaviour; integrated routes through injected factories.
- **Lifecycle hooks**: integrated mode can subscribe to SDK hooks (handoff, MCP lifecycle, tracing/eval stores).
## Recommendation
Use standalone mode for local UI-only workflows. Use integrated mode when you need shared runtime state with PraisonAI agents, tools, tracing, approvals, and schedules.
See also [backend-integration.md](backend-integration.md) and [praisonai-package-integration.md](praisonai-package-integration.md).
+114
View File
@@ -0,0 +1,114 @@
# Video Studio
The Video Studio integrates [PraisonAI Video](https://github.com/MervinPraison/praisonai-video) as an external Node render engine. PraisonAIUI provides the dashboard shell, project files, YAML editor, job UX, and HTTP proxying. The video engine handles parse, compile, lint, and MP4 export. **Agents live in PraisonAI (Chat + tools), not inside the renderer.**
## Prerequisites
1. Install PraisonAI Video CLI (`praisonai-video`) or run from the monorepo.
2. Start the engine sidecar (recommended):
```bash
praisonai-video serve --port 3921
```
3. Run the example app:
```bash
aiui run examples/python/video-studio/app.py
```
Open the **Video** tab, create a project, edit `scene.yaml`, and use Lint / Render.
For **Chat** to edit scenes, install `praisonaiagents` and set `OPENAI_API_KEY`. Optional: `VIDEO_STUDIO_PROJECT_ID` to target a specific project.
## Configuration
| Variable | Default | Purpose |
|----------|---------|---------|
| `VIDEO_ENGINE_URL` | `http://127.0.0.1:3921` | HTTP API base URL |
| `VIDEO_ENGINE_TOKEN` | (none) | Optional Bearer token forwarded to the engine |
| `PRAISONAI_PROJECTS_DIR` | `~/.praisonai/projects` | On-disk project roots |
| `PRAISONAI_VIDEO_CLI` | (auto-detect) | Path to `praisonai-video` or `dist/cli.js` for subprocess fallback |
| `VIDEO_STUDIO_PROJECT_ID` | (auto) | Active project for agent tools when not passed explicitly |
| `PRAISONAI_MODEL` | `gpt-4o-mini` | LLM for Video Editor agent in the example app |
Python API:
```python
from praisonaiui.video_config import set_video_engine
set_video_engine(url="http://127.0.0.1:3921", projects_dir="~/my-videos")
```
## Projects
Each project is a directory containing:
- `scene.yaml` — scene definition
- `scene.visual-test.yaml` — optional visual regression spec
- `.praisonai/project.json` — metadata
- `exports/` — rendered MP4 artefacts
## API routes
All routes are under `/api/video/`:
- `GET /health` — sidecar status
- `POST /lint`, `POST /compile` — scene validation
- `GET /preview-url`, `POST /preview/start` — preview iframe URL (CLI; dashboard preview hidden)
- `POST /render` — async render job (`type: video_render`); optional `backend` (`playwright` | `remotion`) overrides `scene.yaml` `render.backend`
- `GET /jobs/{id}` — job status and download URL
- `GET|POST /projects` — list and create projects
- `GET /projects/{id}/studio-refresh` — poll after agent tool updates (`refresh: true` once)
When the HTTP sidecar is unavailable, lint and render fall back to subprocess calls to `praisonai-video`.
## Dashboard plugin
The UI lives in `dashboard-plugins/video-studio/` (built-in) or `~/.praisonai/dashboard-plugins/video-studio/` for overrides. It uses `window.aiui.sdk.fetchJSON`.
**Frame-by-frame composition preview is hidden** in the dashboard (`SHOW_COMPOSITION_PREVIEW = false`) until serve preview + seek are reliable. Use **Render MP4** and the **Your video** player for output. Set the flag to `true` in `index.js` to re-enable the optional iframe scrubber.
After agent tool writes, the editor reloads via `video-studio:refresh` (custom event) or polling `studio-refresh`.
## Agent workflow (PraisonAI-first)
1. Open **Chat** with the Video Editor agent (example app registers tools when `praisonaiagents` is installed).
2. Agent calls `video_get_scene` / `video_update_scene` / `video_lint_scene` / `video_render_project` (same outcomes as UI buttons).
3. Video Studio tab picks up changes automatically.
Tools are defined in `praisonaiui/video_agent_tools.py`. For Cursor/Codex outside the UI, use the skill at `skills/praisonai-video/SKILL.md` in the PraisonAI Video repo.
### Optional render backends
Default is **Playwright** (no `render:` block in `scene.yaml`). Use Remotion only when required:
```yaml
render:
backend: remotion # needs Remotion licence + remotion-root export; lint will error on default YAML export
```
`video_render_project(backend="playwright")` overrides YAML for one agent run. Hyperframes is deferred.
## CLI parity
The text box edits the same `scene.yaml` the CLI uses. Equivalent commands (from the video repo):
```bash
praisonai-video project create "My video"
praisonai-video project lint <project-id>
praisonai-video project preview <project-id>
praisonai-video project test <project-id>
praisonai-video project render <project-id>
praisonai-video project reset <project-id>
```
Lint editor text without saving: `praisonai-video lint --stdin < scene.yaml`
See `docs/integrations/praisonaiui.md` in the PraisonAI Video repo for the full mapping table.
## Related
- [Agent UI host](agent-ui-host.md) — extension patterns (`registerView`, plugins, `@aiui.page`)
- PraisonAI Video integration doc: `docs/integrations/praisonaiui.md` in the video repo
- PraisonAI Video skill: `skills/praisonai-video/SKILL.md` — YAML-first invariants for agents and CI
+5 -2
View File
@@ -276,10 +276,13 @@ aiui features list # List all registered features
aiui features status # Feature health summary
```
### `aiui health`
### `aiui health-check`
Check server health. Can also be invoked as `aiui health` for convenience.
```bash
aiui health [--detailed] # Check server health
aiui health-check [--detailed] # Check server health
aiui health [--detailed] # Alias for health-check
```
### `aiui session-ext`
+1 -1
View File
@@ -101,6 +101,6 @@ If something isn't working:
```bash
aiui doctor # Check server health
aiui health --detailed # Detailed health check
aiui health-check --detailed # Detailed health check (alias: aiui health)
aiui test all # Run all integration tests
```
+209
View File
@@ -0,0 +1,209 @@
# Health Endpoints Guide
## Overview
PraisonAIUI provides multiple health endpoints optimized for different use cases:
- **`/health/live`** - Fast liveness check (<500ms)
- **`/health/ready`** - Readiness check with optional deep checks
- **`/health`** - Legacy endpoint (backward compatibility)
## Endpoints
### Liveness Check: `/health/live`
Fast endpoint to verify the server is running. Returns immediately with minimal processing.
**Use for:**
- Load balancer health probes
- Kubernetes liveness probes
- High-frequency monitoring
**Response time:** <500ms (typically <100ms)
**Example:**
```bash
curl http://localhost:8082/health/live
```
**Response:**
```json
{
"status": "ok",
"timestamp": "2024-01-20T10:30:00.000Z"
}
```
### Readiness Check: `/health/ready`
Verifies the server is ready to handle requests. Performs parallel health checks on all features with timeout protection.
**Use for:**
- Kubernetes readiness probes
- Pre-deployment validation
- Detailed diagnostics
**Query parameters:**
- `?deep=false` - Skip feature checks for faster response
**Response time:**
- With deep checks: <1s (parallel execution)
- Without deep checks: <200ms
**Example:**
```bash
# Full readiness check
curl http://localhost:8082/health/ready
# Basic readiness (no feature checks)
curl http://localhost:8082/health/ready?deep=false
```
**Response with deep checks:**
```json
{
"status": "ok",
"timestamp": "2024-01-20T10:30:00.000Z",
"provider": {
"name": "PraisonAIProvider",
"status": "ok"
},
"features": {
"agents": {"status": "ok", "feature": "agents"},
"chat": {"status": "ok", "feature": "chat"},
"memory": {"healthy": false, "detail": "timeout"}
}
}
```
### API Health: `/api/health`
Cached version of readiness check for dashboard polling. 30-second TTL to reduce load.
**Use for:**
- Dashboard health indicators
- Frequent client polling
- Status pages
**Response:** Same as `/health/ready` but cached for 30 seconds
## Deployment Configurations
### Kubernetes
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: praisonaiui
spec:
template:
spec:
containers:
- name: praisonaiui
image: praisonaiui:latest
livenessProbe:
httpGet:
path: /health/live
port: 8082
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 2
readinessProbe:
httpGet:
path: /health/ready?deep=false
port: 8082
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
```
### AWS ALB/ELB
```terraform
resource "aws_lb_target_group" "praisonaiui" {
health_check {
enabled = true
path = "/health/live"
interval = 30
timeout = 5
healthy_threshold = 2
unhealthy_threshold = 2
}
}
```
### Docker Compose
```yaml
services:
praisonaiui:
image: praisonaiui:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8082/health/live"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
```
### Nginx
```nginx
upstream praisonaiui {
server backend1:8082 max_fails=2 fail_timeout=5s;
server backend2:8082 max_fails=2 fail_timeout=5s;
# Health check configuration
check interval=5000 rise=2 fall=3 timeout=2000 type=http;
check_http_send "GET /health/live HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx;
}
```
## Migration Guide
If you're upgrading from a version using only `/health`:
1. **Update monitoring:** Change health checks to use `/health/live` for liveness
2. **Update readiness:** Use `/health/ready?deep=false` for readiness probes
3. **Update dashboards:** Point status indicators to `/api/health` (cached)
4. **Backward compatibility:** The `/health` endpoint remains available but may be slow
## Performance Characteristics
| Endpoint | Cold Start | Warm (p50) | Warm (p95) | Use Case |
|----------|------------|------------|------------|----------|
| `/health/live` | <100ms | <20ms | <50ms | Liveness probes |
| `/health/ready?deep=false` | <200ms | <50ms | <100ms | Basic readiness |
| `/health/ready` | <1s | <500ms | <800ms | Full diagnostics |
| `/api/health` (cached) | <1s* | <20ms | <50ms | Dashboard polling |
*First request only, then cached for 30s
## Troubleshooting
### Slow `/health` responses
If you're seeing slow responses on the legacy `/health` endpoint:
- Switch to `/health/live` for fast checks
- Use `/health/ready?deep=false` if you don't need feature health
- Enable caching with `/api/health` for frequent polling
### Feature timeouts
Features that timeout in `/health/ready` will show:
```json
{
"feature_name": {
"healthy": false,
"detail": "timeout"
}
}
```
This indicates the feature took longer than 500ms to respond. The server is still operational, but that specific feature may be experiencing issues.
### High latency under load
Under high load, use `/health/live` exclusively for monitoring. It has minimal overhead and won't impact performance.
+69
View File
@@ -0,0 +1,69 @@
# Application Design Protocol (ADP)
Declarative UI design standard for describing application layout, navigation, components, and theming from YAML or JSON manifests.
**Status:** v1alpha1 research draft — see [implementation-status.md](./implementation-status.md) for what is implemented today.
## Documents
| Document | Description |
|----------|-------------|
| [Research report](../application-design-protocol-report.md) | 46 standards compared; YAML vs JSON debate; design principles |
| [spec.md](./spec.md) | Normative specification (draft) |
| [validation-rules.md](./validation-rules.md) | Cross-reference rules R1–R30 |
| [aiui-mapping.md](./aiui-mapping.md) | PraisonAIUI field mapping |
| [implementation-status.md](./implementation-status.md) | Feature implementation tracker |
## Schemas
| File | Purpose |
|------|---------|
| [schema/application.schema.json](./schema/application.schema.json) | Root Application kind |
| [schema/component-catalog.schema.json](./schema/component-catalog.schema.json) | Catalog meta-schema |
| [schema/theme.schema.json](./schema/theme.schema.json) | Theme preset or DTCG tokens |
| [catalogs/default.catalog.json](./catalogs/default.catalog.json) | Default component and layout catalog |
## Examples
| File | Description |
|------|-------------|
| [examples/minimal.application.yaml](./examples/minimal.application.yaml) | Smallest valid app |
| [examples/docs-site.application.yaml](./examples/docs-site.application.yaml) | Full docs site |
| [examples/landing-page.application.yaml](./examples/landing-page.application.yaml) | Zone-based layout |
## Validate locally
```bash
pip install jsonschema pyyaml
pytest tests/unit/test_adp_schemas.py -v
```
## Quick start
```yaml
schemaVersion: 1
apiVersion: adp.io/v1alpha1
kind: Application
metadata:
name: my-app
version: "1.0.0"
spec:
site:
title: "My App"
theme:
preset: zinc
darkMode: true
templates:
docs:
layout: TwoColumnLayout
slots:
main: { type: DocContent }
routes:
- match: "/docs/**"
template: docs
content:
docs:
dir: "./docs"
```
+138
View File
@@ -0,0 +1,138 @@
# PraisonAIUI → ADP Field Mapping
Field-by-field mapping from current `aiui.template.yaml` (`Config` model in `src/praisonaiui/schema/models.py`) to ADP `Application` spec.
**Compatibility:** Flat `aiui.template.yaml` files work with the PraisonAIUI compiler today. ADP envelope format is **planned** — see [implementation-status.md](./implementation-status.md).
---
## Envelope (new in ADP)
| ADP field | PraisonAIUI today | Status | Change |
|-----------|-------------------|--------|--------|
| `schemaVersion: 1` | `schemaVersion: 1` | Implemented | Compatible |
| `apiVersion: adp.io/v1alpha1` | Not present | Planned | Add when envelope parser lands |
| `kind: Application` | Not present | Planned | Add when envelope parser lands |
| `metadata.name` | Not present | Planned | Derive from `site.title` slug |
| `metadata.version` | Not present | Planned | Recommend SemVer |
## Top-level → spec wrapper
| PraisonAIUI (flat) | ADP (envelope) | Status | Change |
|--------------------|----------------|--------|--------|
| `site:` | `spec.site:` | Planned | Wrap under `spec` |
| `site.theme` | `spec.theme:` | Planned | Promote theme to spec level |
| `navigation:` | `spec.navigation:` | Partial | Tabs compiled; not rendered |
| `navbar:` | `spec.navbar:` | Partial | Compiled; not rendered |
| `footer:` | `spec.footer:` | Partial | Compiled; not rendered |
| `search:` | `spec.search:` | Partial | Compiled; not rendered |
| `content:` | `spec.content:` | Implemented | Same |
| `components:` | `spec.components:` | Implemented | Same |
| `templates:` | `spec.templates:` | Implemented | Same |
| `routes:` | `spec.routes:` | Partial | Compiled; routing not honoured |
| `dependencies:` | `spec.dependencies:` | Implemented | Same |
| `logo:` (top-level) | Future ADP field | Implemented | Top-level on `Config`, not under `site` |
| `style:`, `chat:`, `auth:`, `dashboard:` | Not in ADP v1alpha1 | N/A | Separate kinds in future |
## Navigation fields
| Field | PraisonAIUI | ADP | Status | Notes |
|-------|-------------|-----|--------|-------|
| `navigation.tabs[]` | Mintlify-style tabs | `spec.navigation.tabs` | Partial | Compiled, not rendered |
| `content.docs.nav.mode` | `NavConfig.mode` | Same | Gap | Defined but unused by compiler |
| Top-level `navigation.mode` | Not present | Removed from ADP | N/A | Use `content.docs.nav.mode` instead |
## Theme fields
| Field | PraisonAIUI | ADP | Status |
|-------|-------------|-----|--------|
| `site.theme.preset` | Implemented | `spec.theme.preset` | Planned (envelope) |
| `site.theme.radius` | Implemented | Same | Same |
| `site.theme.darkMode` | Implemented | Same | Same |
| `theme.tokens` | Not present | DTCG file ref | Planned |
## Component fields
| Field | PraisonAIUI | ADP | Status |
|-------|-------------|-----|--------|
| `components.{id}.type` | String literal | Catalog allowlist | Planned (R6) |
| `components.{id}.props` | `dict[str, Any]` | Catalog schema | Planned (R7) |
## Template fields
| Field | PraisonAIUI | ADP | Status |
|-------|-------------|-----|--------|
| `templates.{id}.layout` | String | Catalog enum | Same values |
| `templates.{id}.slots` | `{ ref \| type }` | Same | Same |
| `templates.{id}.zones` | Widget arrays | Same | Same |
| Slots + zones on same template | Allowed | R13: one or the other | Stricter in ADP |
## Route fields
| Field | PraisonAIUI | ADP | Status |
|-------|-------------|-----|--------|
| `routes[].match` | Glob string | Same | Same |
| `routes[].template` | Template key | Same | Same |
| Runtime route resolution | Not implemented | R28 required | Gap |
## Migration example
### Today (PraisonAIUI)
```yaml
schemaVersion: 1
site:
title: "My Docs"
theme:
preset: zinc
templates:
docs:
layout: TwoColumnLayout
slots:
main: { type: DocContent }
routes:
- match: "/docs/**"
template: docs
```
### Target (ADP)
```yaml
schemaVersion: 1
apiVersion: adp.io/v1alpha1
kind: Application
metadata:
name: my-docs
version: "1.0.0"
spec:
site:
title: "My Docs"
theme:
preset: zinc
templates:
docs:
layout: TwoColumnLayout
slots:
main: { type: DocContent }
routes:
- match: "/docs/**"
template: docs
content:
docs:
dir: "./docs"
```
## Breaking vs compatible changes
| Change | Breaking? | Mitigation |
|--------|-----------|------------|
| Add envelope fields | No | Envelope parser normalises to flat `Config` |
| Catalog validation | Soft break | Warn first, error in strict mode |
| Route runtime resolution | No (fix) | Frontend change |
| R13 slots/zones exclusivity | Soft break | Warn on violation |
| Remove chat/dashboard from ADP | No | Separate kinds in future |
@@ -0,0 +1,128 @@
{
"name": "adp-default-catalog",
"version": "1.0.0",
"description": "Default ADP component and layout catalog for PraisonAIUI",
"components": {
"Header": {
"description": "Site header with logo and navigation",
"props": {
"type": "object",
"properties": {
"logoText": { "type": "string" },
"logoHref": { "type": "string", "default": "/" }
},
"required": ["logoText"],
"additionalProperties": false
},
"aria": { "role": "banner" },
"slots": ["header"]
},
"Footer": {
"description": "Site footer",
"props": {
"type": "object",
"properties": {
"text": { "type": "string" }
},
"additionalProperties": false
},
"aria": { "role": "contentinfo" },
"slots": ["footer"]
},
"DocsSidebar": {
"description": "Documentation sidebar navigation",
"props": {
"type": "object",
"properties": {
"source": { "type": "string", "enum": ["docs-nav", "manual"], "default": "docs-nav" }
},
"additionalProperties": false
},
"aria": { "role": "navigation" },
"slots": ["left", "leftSidebar"]
},
"DocContent": {
"description": "Main documentation content area",
"props": { "type": "object", "additionalProperties": false },
"aria": { "role": "main" },
"slots": ["main"]
},
"Toc": {
"description": "Table of contents for current page",
"props": { "type": "object", "additionalProperties": false },
"aria": { "role": "navigation" },
"slots": ["right", "rightSidebar"]
},
"HeroBanner": {
"description": "Hero banner with title and CTA",
"props": {
"type": "object",
"properties": {
"title": { "type": "string" },
"subtitle": { "type": "string" },
"ctaLabel": { "type": "string" },
"ctaHref": { "type": "string" }
},
"required": ["title"],
"additionalProperties": false
},
"slots": ["hero"]
},
"SocialLinks": {
"description": "Social media link row",
"props": {
"type": "object",
"properties": {
"links": {
"type": "array",
"items": {
"type": "object",
"properties": {
"platform": { "type": "string" },
"href": { "type": "string" }
},
"required": ["platform", "href"],
"additionalProperties": false
}
}
},
"additionalProperties": false
},
"slots": ["footer"]
},
"Copyright": {
"description": "Copyright notice",
"props": {
"type": "object",
"properties": {
"text": { "type": "string" }
},
"required": ["text"],
"additionalProperties": false
},
"slots": ["footer"]
}
},
"layouts": {
"ThreeColumnLayout": {
"description": "Header + left sidebar + main + right TOC + footer",
"slots": ["header", "left", "main", "right", "footer"]
},
"TwoColumnLayout": {
"description": "Sidebar + main content",
"slots": ["left", "main"]
},
"CenteredLayout": {
"description": "Centred single column",
"slots": ["main"]
},
"FullWidthLayout": {
"description": "Full-width content",
"slots": ["header", "main", "footer"]
},
"FlexibleLayout": {
"description": "WordPress-style zone grid",
"zones": ["header", "topNav", "hero", "leftSidebar", "main", "rightSidebar", "bottomNav", "footer"]
}
}
}
@@ -0,0 +1,104 @@
schemaVersion: 1
apiVersion: adp.io/v1alpha1
kind: Application
metadata:
name: praisonaiui-docs
version: "1.0.0"
labels:
generator: praisonaiui
spec:
site:
title: "PraisonAIUI Docs"
description: "Documentation for PraisonAIUI - YAML-driven website generator"
ui: shadcn
theme:
preset: zinc
radius: md
darkMode: true
navigation:
tabs:
- tab: "Documentation"
groups:
- group: "Getting Started"
icon: rocket
prefix: getting-started
pages:
- index
- getting-started/installation
- getting-started/quickstart
- group: "Core Concepts"
icon: lightbulb
prefix: concepts
pages:
- concepts/architecture
- concepts/configuration
- concepts/templates
- tab: "API Reference"
groups:
- group: "API Reference"
icon: code
prefix: api
pages:
- api/cli
- api/python
navbar:
primary:
type: github
href: "https://github.com/MervinPraison/PraisonAIUI"
links:
- label: "Submit Issues"
href: "https://github.com/MervinPraison/PraisonAIUI/issues"
footer:
socials:
github: "https://github.com/MervinPraison/PraisonAI"
links:
- header: "Resources"
items:
- label: "GitHub"
href: "https://github.com/MervinPraison/PraisonAI"
search:
enabled: true
provider: fusejs
content:
docs:
dir: "./docs"
include:
- "**/*.md"
- "**/*.mdx"
nav:
mode: manual
components:
header_main:
type: Header
props:
logoText: "PraisonAIUI"
footer_main:
type: Footer
props:
text: "© 2026 PraisonAI"
sidebar_docs:
type: DocsSidebar
props:
source: docs-nav
templates:
docs:
layout: ThreeColumnLayout
slots:
header: { ref: header_main }
left: { ref: sidebar_docs }
main: { type: DocContent }
footer: { ref: footer_main }
routes:
- match: "/docs/**"
template: docs
@@ -0,0 +1,58 @@
schemaVersion: 1
apiVersion: adp.io/v1alpha1
kind: Application
metadata:
name: colorful-landing
version: "1.0.0"
spec:
site:
title: "Colorful Docs"
description: "Zone-based landing page with hero and footer widgets"
ui: shadcn
theme:
preset: rose
radius: xl
darkMode: true
content:
docs:
dir: "./docs"
nav:
mode: auto
collapsible: true
templates:
docs:
layout: FlexibleLayout
zones:
hero:
- type: HeroBanner
props:
title: "Colorful Documentation"
subtitle: "Vibrant, playful, and beautifully designed"
ctaLabel: "Get Started"
ctaHref: "/docs/getting-started/quickstart"
rightSidebar:
- type: Toc
footer:
- type: SocialLinks
props:
links:
- platform: Twitter
href: "https://twitter.com"
- platform: GitHub
href: "https://github.com"
- type: Copyright
props:
text: "Made with love by Colorful Team"
routes:
- match: "/docs/**"
template: docs
a11y:
skipToContent: true
reduceMotion: false
@@ -0,0 +1,32 @@
schemaVersion: 1
apiVersion: adp.io/v1alpha1
kind: Application
metadata:
name: minimal-docs
version: "1.0.0"
spec:
site:
title: "Minimal Docs"
description: "Smallest valid ADP Application manifest"
ui: shadcn
theme:
preset: zinc
radius: md
darkMode: true
content:
docs:
dir: "./docs"
templates:
docs:
layout: TwoColumnLayout
slots:
main: { type: DocContent }
routes:
- match: "/docs/**"
template: docs
@@ -0,0 +1,36 @@
# ADP Implementation Status
Honest status of ADP v1alpha1 features against the PraisonAIUI reference implementation.
**Last updated:** August 2026
| Feature | Spec reference | Status | Notes |
|---------|----------------|--------|-------|
| JSON Schema validation in CLI | spec §6 | Planned | Schemas published; no `aiui validate --adp` yet |
| ADP envelope parsing (`apiVersion/kind/spec`) | spec §3 | Planned | Flat `aiui.template.yaml` works today |
| Component catalog validation (R6–R7) | spec §5 | Planned | Catalog split to `catalogs/default.catalog.json` |
| `$yaml:` / `$json:` file includes | spec §7 | Planned | Not implemented in compiler |
| Strict mode unknown-field rejection (R4) | validation-rules R4 | Planned | Pydantic allows extra fields on some models |
| Route-driven template resolution (R28) | validation-rules R28 | Gap | Frontend hardcodes `templates.docs` |
| Sidebar nav mode (R29) | validation-rules R29 | Gap | Filesystem-only sidebar today |
| Catalog ARIA roles at render (R30) | validation-rules R30 | Gap | No catalog-driven a11y in renderer |
| `navigation.tabs` rendering | spec §4.3 | Gap | Compiled to manifest but not consumed by React |
| DTCG token file validation (R26) | validation-rules R26 | Planned | Preset shorthand works via Tailwind |
| Published `$schema` URLs | README | Partial | Local schemas only; `https://adp.io/` is placeholder |
## What works today
- Flat `aiui.template.yaml` → Pydantic `Config` → compiler → JSON manifests → React SPA
- Components, templates, routes, theme presets, content scanning
- ADP example manifests validate against `application.schema.json` (standalone)
- Unit tests in `tests/unit/test_adp_schemas.py`
## Migration path
1. Add envelope normalisation layer before `Config.model_validate`
2. Wire `route-manifest.json` into frontend routing (R28)
3. Honour `navigation.tabs` and `content.docs.nav.mode` (R29)
4. Add `aiui validate --schema adp` using published JSON Schema
5. Apply catalog ARIA hints at render time (R30)
See [aiui-mapping.md](./aiui-mapping.md) for field-level compatibility.
@@ -0,0 +1,449 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "application.schema.json",
"title": "ADP Application",
"description": "Application Design Protocol — Application kind manifest",
"type": "object",
"required": ["schemaVersion", "apiVersion", "kind", "metadata", "spec"],
"additionalProperties": false,
"patternProperties": {
"^x-adp-": {}
},
"properties": {
"schemaVersion": {
"type": "integer",
"const": 1,
"description": "ADP envelope version"
},
"apiVersion": {
"type": "string",
"const": "adp.io/v1alpha1"
},
"kind": {
"type": "string",
"const": "Application"
},
"metadata": {
"type": "object",
"required": ["name"],
"additionalProperties": false,
"patternProperties": {
"^x-adp-": {}
},
"properties": {
"name": { "type": "string", "minLength": 1 },
"version": { "type": "string" },
"labels": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"annotations": {
"type": "object",
"additionalProperties": { "type": "string" }
}
}
},
"spec": {
"type": "object",
"required": ["site", "templates", "routes"],
"additionalProperties": false,
"patternProperties": {
"^x-adp-": {}
},
"properties": {
"site": { "$ref": "#/$defs/site" },
"theme": { "$ref": "#/$defs/theme" },
"navigation": { "$ref": "#/$defs/navigation" },
"navbar": { "$ref": "#/$defs/navbar" },
"footer": { "$ref": "#/$defs/footer" },
"search": { "$ref": "#/$defs/search" },
"components": { "$ref": "#/$defs/components" },
"templates": { "$ref": "#/$defs/templates" },
"routes": { "$ref": "#/$defs/routes" },
"content": { "$ref": "#/$defs/content" },
"dependencies": { "$ref": "#/$defs/dependencies" },
"seo": { "$ref": "#/$defs/seo" },
"i18n": { "$ref": "#/$defs/i18n" },
"a11y": { "$ref": "#/$defs/a11y" }
}
}
},
"$defs": {
"site": {
"type": "object",
"required": ["title"],
"additionalProperties": false,
"patternProperties": {
"^x-adp-": {}
},
"properties": {
"title": { "type": "string" },
"description": { "type": "string" },
"ui": {
"type": "string",
"enum": ["shadcn", "mui", "chakra"],
"default": "shadcn"
},
"routeBaseDocs": { "type": "string", "default": "/docs" },
"customCss": { "type": "string" },
"plugins": {
"type": "array",
"items": { "type": "string" }
}
}
},
"theme": {
"oneOf": [
{
"title": "Preset theme",
"type": "object",
"additionalProperties": false,
"properties": {
"preset": {
"type": "string",
"enum": [
"zinc", "slate", "stone", "gray", "neutral",
"red", "orange", "amber", "yellow", "lime", "green",
"emerald", "teal", "cyan", "sky", "blue", "indigo",
"violet", "purple", "fuchsia", "pink", "rose"
]
},
"radius": {
"type": "string",
"enum": ["none", "sm", "md", "lg", "xl"]
},
"darkMode": { "type": "boolean" }
},
"required": ["preset"]
},
{
"title": "DTCG token reference",
"type": "object",
"additionalProperties": false,
"properties": {
"tokens": {
"type": "string",
"description": "Path or URL to DTCG 2025.10 compliant tokens.json"
},
"darkMode": { "type": "boolean" }
},
"required": ["tokens"]
}
]
},
"slotRef": {
"oneOf": [
{ "type": "null" },
{
"type": "object",
"required": ["ref"],
"additionalProperties": false,
"properties": {
"ref": { "type": "string" }
}
},
{
"type": "object",
"required": ["type"],
"additionalProperties": false,
"properties": {
"type": { "type": "string" },
"props": {
"type": "object",
"additionalProperties": true
}
}
}
]
},
"widget": {
"type": "object",
"required": ["type"],
"additionalProperties": false,
"properties": {
"type": { "type": "string" },
"props": {
"type": "object",
"additionalProperties": true
}
}
},
"components": {
"type": "object",
"additionalProperties": {
"type": "object",
"required": ["type"],
"additionalProperties": false,
"properties": {
"type": { "type": "string" },
"props": {
"type": "object",
"additionalProperties": true
}
}
}
},
"template": {
"type": "object",
"required": ["layout"],
"allOf": [
{
"oneOf": [
{ "required": ["slots"] },
{ "required": ["zones"] }
]
}
],
"additionalProperties": false,
"properties": {
"layout": {
"type": "string",
"enum": [
"ThreeColumnLayout",
"TwoColumnLayout",
"CenteredLayout",
"FullWidthLayout",
"FlexibleLayout"
]
},
"slots": {
"type": "object",
"additionalProperties": { "$ref": "#/$defs/slotRef" }
},
"zones": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": { "$ref": "#/$defs/widget" }
}
}
}
},
"templates": {
"type": "object",
"minProperties": 1,
"additionalProperties": { "$ref": "#/$defs/template" }
},
"route": {
"type": "object",
"required": ["match", "template"],
"additionalProperties": false,
"properties": {
"match": { "type": "string" },
"template": { "type": "string" },
"slots": {
"type": "object",
"additionalProperties": { "$ref": "#/$defs/slotRef" }
}
}
},
"routes": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/route" }
},
"navGroup": {
"type": "object",
"required": ["group"],
"additionalProperties": false,
"properties": {
"group": { "type": "string" },
"icon": { "type": "string" },
"prefix": { "type": "string" },
"pages": {
"type": "array",
"items": { "type": "string" }
}
}
},
"navTab": {
"type": "object",
"required": ["tab"],
"additionalProperties": false,
"properties": {
"tab": { "type": "string" },
"url": { "type": "string" },
"groups": {
"type": "array",
"items": { "$ref": "#/$defs/navGroup" }
}
}
},
"navigation": {
"type": "object",
"additionalProperties": false,
"properties": {
"tabs": {
"type": "array",
"items": { "$ref": "#/$defs/navTab" }
}
}
},
"navbar": {
"type": "object",
"additionalProperties": false,
"properties": {
"primary": {
"type": "object",
"additionalProperties": false,
"properties": {
"type": { "type": "string", "enum": ["button", "github"] },
"label": { "type": "string" },
"href": { "type": "string" }
}
},
"links": {
"type": "array",
"items": {
"type": "object",
"required": ["label", "href"],
"additionalProperties": false,
"properties": {
"label": { "type": "string" },
"href": { "type": "string" }
}
}
}
}
},
"footer": {
"type": "object",
"additionalProperties": false,
"properties": {
"socials": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"links": {
"type": "array",
"items": {
"type": "object",
"required": ["header"],
"additionalProperties": false,
"properties": {
"header": { "type": "string" },
"items": {
"type": "array",
"items": {
"type": "object",
"required": ["label", "href"],
"additionalProperties": false,
"properties": {
"label": { "type": "string" },
"href": { "type": "string" }
}
}
}
}
}
}
}
},
"search": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"provider": {
"type": "string",
"enum": ["fusejs", "lunrjs", "pagefind"]
}
}
},
"dependencies": {
"type": "object",
"additionalProperties": false,
"properties": {
"shadcn": {
"type": "array",
"items": { "type": "string" }
}
}
},
"contentSource": {
"type": "object",
"required": ["dir"],
"additionalProperties": false,
"properties": {
"dir": { "type": "string" },
"include": {
"type": "array",
"items": { "type": "string" }
},
"exclude": {
"type": "array",
"items": { "type": "string" }
},
"indexFiles": {
"type": "array",
"items": { "type": "string" }
},
"nav": {
"type": "object",
"additionalProperties": false,
"properties": {
"mode": { "type": "string", "enum": ["auto", "manual"] },
"sort": { "type": "string", "enum": ["filesystem", "alpha", "date"] },
"collapsible": { "type": "boolean" },
"maxDepth": { "type": "integer", "minimum": 1, "maximum": 10 }
},
"allOf": [
{
"if": {
"properties": { "mode": { "const": "manual" } },
"required": ["mode"]
},
"then": {
"description": "Manual sidebar mode requires navigation.tabs at spec level"
}
}
]
}
}
},
"content": {
"type": "object",
"additionalProperties": false,
"properties": {
"docs": { "$ref": "#/$defs/contentSource" },
"blog": { "$ref": "#/$defs/contentSource" }
}
},
"seo": {
"type": "object",
"additionalProperties": false,
"properties": {
"titleTemplate": { "type": "string" },
"defaultImage": { "type": "string" },
"twitter": {
"type": "object",
"additionalProperties": { "type": "string" }
}
}
},
"i18n": {
"type": "object",
"additionalProperties": false,
"properties": {
"defaultLocale": { "type": "string" },
"locales": { "type": "array", "items": { "type": "string" } },
"rtlLocales": { "type": "array", "items": { "type": "string" } },
"fallbackLocale": { "type": "string" },
"translationsDir": { "type": "string" }
}
},
"a11y": {
"type": "object",
"additionalProperties": false,
"properties": {
"skipToContent": { "type": "boolean" },
"focusVisible": { "type": "boolean" },
"reduceMotion": { "type": "boolean" },
"ariaLabels": {
"type": "object",
"additionalProperties": { "type": "string" }
}
}
}
}
}
@@ -0,0 +1,82 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "component-catalog.schema.json",
"title": "ADP Component Catalog",
"description": "Allowlisted component types for ADP Application manifests",
"type": "object",
"required": ["name", "version", "components"],
"additionalProperties": false,
"patternProperties": {
"^x-adp-": {}
},
"properties": {
"name": { "type": "string" },
"version": { "type": "string" },
"description": { "type": "string" },
"components": {
"type": "object",
"additionalProperties": { "$ref": "#/$defs/componentType" }
},
"layouts": {
"type": "object",
"additionalProperties": { "$ref": "#/$defs/layoutType" }
}
},
"$defs": {
"componentType": {
"type": "object",
"required": ["description", "props"],
"additionalProperties": false,
"properties": {
"description": { "type": "string" },
"props": { "type": "object" },
"aria": {
"type": "object",
"additionalProperties": false,
"properties": {
"role": { "type": "string" },
"label": { "type": "string" }
}
},
"slots": {
"type": "array",
"items": { "type": "string" }
},
"deprecated": { "type": "boolean" },
"x-sunset": { "type": "string", "format": "date" }
}
},
"layoutType": {
"oneOf": [
{
"title": "Slot-based layout",
"type": "object",
"required": ["description", "slots"],
"additionalProperties": false,
"properties": {
"description": { "type": "string" },
"slots": {
"type": "array",
"items": { "type": "string" },
"minItems": 1
}
}
},
{
"title": "Zone-based layout",
"type": "object",
"required": ["description", "zones"],
"additionalProperties": false,
"properties": {
"description": { "type": "string" },
"zones": {
"type": "array",
"items": { "type": "string" },
"minItems": 1
}
}
}
]
}
}
}
@@ -0,0 +1,47 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "theme.schema.json",
"title": "ADP Theme",
"description": "Theme configuration — preset shorthand or DTCG 2025.10 token reference",
"oneOf": [
{
"title": "Preset theme",
"type": "object",
"additionalProperties": false,
"properties": {
"preset": {
"type": "string",
"enum": [
"zinc", "slate", "stone", "gray", "neutral",
"red", "orange", "amber", "yellow", "lime", "green",
"emerald", "teal", "cyan", "sky", "blue", "indigo",
"violet", "purple", "fuchsia", "pink", "rose"
]
},
"radius": {
"type": "string",
"enum": ["none", "sm", "md", "lg", "xl"],
"default": "md"
},
"darkMode": {
"type": "boolean",
"default": true
}
},
"required": ["preset"]
},
{
"title": "DTCG token reference",
"type": "object",
"additionalProperties": false,
"properties": {
"tokens": {
"type": "string",
"description": "Path or URL to DTCG 2025.10 compliant tokens.json"
},
"darkMode": { "type": "boolean" }
},
"required": ["tokens"]
}
]
}
+314
View File
@@ -0,0 +1,314 @@
# Application Design Protocol (ADP) — Normative Specification
**Version:** v1alpha1 (draft)
**Status:** Draft — not yet normative
**Date:** August 2026
This document uses [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119) keywords: MUST, MUST NOT, SHOULD, MAY.
---
## 1. Scope
ADP defines a declarative format for describing **application UI design**: site metadata, theming, navigation, reusable components, page templates, and URL routing. It does NOT define:
- Server-side APIs or data models
- Agent orchestration or workflow graphs
- Infrastructure or deployment configuration
- Executable code or templating logic in manifests
Implementations MUST treat ADP manifests as **data**, not code.
---
## 2. Terminology
| Term | Definition |
|------|------------|
| **Manifest** | A YAML or JSON document conforming to this spec |
| **Application** | The root `kind: Application` manifest describing a complete UI |
| **Component** | A named, reusable UI block with a `type` and `props` |
| **Template** | A layout definition assigning components to slots or zones |
| **Slot** | A named region within a layout (e.g. `header`, `main`) |
| **Zone** | An ordered list of widgets in a slot (WordPress-style) |
| **Route** | A URL pattern mapping to a template |
| **Catalog** | Published JSON Schema defining allowed component types |
| **Compiler** | Tool that validates and emits runtime JSON manifests |
| **Renderer** | Runtime that consumes compiled manifests and renders UI |
---
## 3. Document envelope
Every ADP manifest MUST include:
```yaml
schemaVersion: 1 # Integer; ADP envelope version
apiVersion: adp.io/v1alpha1
kind: Application # Application | Theme | ComponentCatalog (future)
metadata:
name: string # Required; unique identifier
version: string # SHOULD use SemVer
labels: object # MAY contain key-value pairs
annotations: object # MAY contain tooling metadata
spec: object # Required; kind-specific payload
```
### 3.1 Versioning policy
- `schemaVersion` — integer bumped on breaking envelope changes
- `apiVersion` — string group/version (e.g. `adp.io/v1alpha1`, `adp.io/v1`)
- `metadata.version` — application SemVer (independent of protocol version)
- Breaking changes MUST increment `apiVersion` (e.g. v1alpha1 → v1)
- Deprecated fields MUST include `deprecated: true` and SHOULD include `x-sunset` date
### 3.2 Authoring formats
Manifests MAY be authored as YAML (`.application.yaml`) or JSON (`.application.json`). Semantics MUST be identical. Runtimes MUST NOT consume YAML directly; compilers MUST normalise to JSON before validation.
---
## 4. Application spec
An `Application` spec MUST contain at minimum:
| Field | Required | Description |
|-------|----------|-------------|
| `site.title` | YES | Application display name |
| `templates` | YES | At least one template |
| `routes` | YES | At least one route |
All other fields are OPTIONAL.
### 4.1 Site (`spec.site`)
```yaml
site:
title: string # Required
description: string # Optional
ui: shadcn # UI framework hint; default shadcn
routeBaseDocs: /docs # Base path for documentation routes
```
### 4.2 Theme (`spec.theme`)
Theme MAY use preset shorthand or reference a DTCG token file:
```yaml
theme:
preset: zinc # Tailwind-style preset name
radius: md # none | sm | md | lg | xl
darkMode: true
# OR
tokens: $yaml: ./theme.tokens.json # DTCG 2025.10 file
```
### 4.3 Navigation
ADP separates **top tab bar** from **sidebar source**:
```yaml
navigation:
tabs: # Top tab bar (Mintlify-style)
- tab: Documentation
groups:
- group: Getting Started
pages: [index, getting-started/installation]
content:
docs:
dir: ./docs
nav:
mode: auto # auto | manual — sidebar source
sort: filesystem
collapsible: true
maxDepth: 4
```
- **`navigation.tabs`** — top-level tab bar with grouped pages
- **`content.docs.nav.mode: auto`** — sidebar generated from filesystem scan
- **`content.docs.nav.mode: manual`** — sidebar driven by `navigation.tabs` groups
- Renderers MUST honour `content.docs.nav.mode` when building sidebar (R29)
### 4.4 Components (`spec.components`)
```yaml
components:
header_main:
type: Header # MUST exist in component catalog
props:
logoText: PraisonAIUI
```
Rules:
- Component IDs MUST be unique within the manifest
- `type` MUST match an entry in the published component catalog
- `props` MUST validate against the catalog JSON Schema for that type
### 4.5 Templates (`spec.templates`)
```yaml
templates:
docs:
layout: ThreeColumnLayout
slots:
header: { ref: header_main }
left: { ref: sidebar_docs }
main: { type: DocContent }
right: null # Explicitly hide slot
landing:
layout: FlexibleLayout
zones:
hero:
- type: HeroBanner
props: { title: "Welcome" }
```
Rules:
- `layout` MUST be a known layout type in the catalog
- Each slot value MUST be `{ ref: id }`, `{ type: ComponentType }`, or `null`
- Zones MUST use zone names valid for `FlexibleLayout`
- A template MUST declare either `slots` or `zones`, not both (R13)
### 4.6 Routes (`spec.routes`)
```yaml
routes:
- match: /docs/changelog
template: changelog
slots: # Optional per-route slot overrides
right: null
- match: /docs/**
template: docs
```
Rules:
- Routes MUST be ordered most-specific-first
- `match` MUST be a glob pattern
- `template` MUST reference a key in `spec.templates`
- Renderers MUST resolve routes at runtime using compiled `route-manifest.json`
### 4.7 Content (`spec.content`)
```yaml
content:
docs:
dir: ./docs
include: ["**/*.md", "**/*.mdx"]
exclude: []
nav:
mode: auto
sort: filesystem
collapsible: true
maxDepth: 4
```
---
## 5. Component catalog
Implementations MUST publish a `component-catalog.schema.json` (meta-schema) and a catalog instance such as [`catalogs/default.catalog.json`](./catalogs/default.catalog.json):
```json
{
"name": "adp-default-catalog",
"version": "1.0.0",
"components": {
"Header": {
"description": "Site header with logo and navigation",
"props": {
"type": "object",
"properties": { "logoText": { "type": "string" } },
"required": ["logoText"],
"additionalProperties": false
},
"aria": { "role": "banner" },
"slots": ["header"]
}
},
"layouts": {
"ThreeColumnLayout": {
"description": "Header + sidebar + main + TOC + footer",
"slots": ["header", "left", "main", "right", "footer"]
}
}
}
```
Custom component types MUST be registered via plugin schema layers. Unknown types MUST cause validation failure in strict mode.
---
## 6. Compilation pipeline
A conforming compiler MUST:
1. Parse YAML or JSON manifest
2. Normalise to JSON
3. Validate against `application.schema.json` (JSON Schema 2020-12)
4. Validate cross-reference rules (see [validation-rules.md](./validation-rules.md))
5. Validate component props against catalog schema
6. Emit runtime manifests:
- `ui-config.json` — site, components, templates, theme, navigation
- `route-manifest.json` — ordered routes with template refs
- `docs-nav.json` — filesystem-derived navigation (if mode auto)
---
## 7. File composition
Manifests MAY include external files using Backstage-style placeholders:
```yaml
theme:
$yaml: ./theme.yaml
navigation:
$yaml: ./navigation.yaml
```
Rules:
- Paths MUST be relative to the containing manifest
- Path traversal (`../../`) MUST be rejected
- Supported placeholders: `$yaml:`, `$json:`, `$text:`
---
## 8. Extensions
Vendor extensions MUST use the `x-adp-` prefix:
```yaml
spec:
site:
x-adp-customAnalytics: true
```
Implementations MUST preserve unknown `x-adp-*` fields. Non-prefixed unknown fields MUST be rejected in strict mode.
---
## 9. Conformance
An implementation is **ADP-conformant** if and only if it:
1. Accepts manifests with `apiVersion: adp.io/v1alpha1` and `kind: Application`
2. Validates all MUST-level requirements in this spec and validation-rules.md
3. Rejects manifests with unknown fields in strict mode
4. Emits runtime JSON manifests from valid Application specs
5. Resolves routes to templates at render time
---
## 10. Related documents
- [Research report](../application-design-protocol-report.md)
- [Implementation status](./implementation-status.md)
- [PraisonAIUI mapping](./aiui-mapping.md)
- [JSON Schema: application](./schema/application.schema.json)
- [JSON Schema: component catalog](./schema/component-catalog.schema.json)
- [JSON Schema: theme](./schema/theme.schema.json)
- [Examples](./examples/)
+115
View File
@@ -0,0 +1,115 @@
# ADP Validation Rules
Cross-reference validation rules for Application Design Protocol manifests. These constraints cannot be expressed in JSON Schema alone and MUST be enforced by conforming compilers.
Rules use RFC 2119 keywords: MUST, MUST NOT, SHOULD, MAY.
Inspired by AWP validation rules R1–R32 pattern.
---
## Envelope rules
| Rule | Level | Description |
|------|-------|-------------|
| **R1** | MUST | `schemaVersion` MUST equal `1` for `apiVersion: adp.io/v1alpha1` |
| **R2** | MUST | `kind` MUST be `Application` for root manifests |
| **R3** | MUST | `metadata.name` MUST be unique within a deployment scope |
| **R4** | MUST NOT | Unknown top-level fields outside `x-adp-*` namespace in strict mode |
## Component rules
| Rule | Level | Description |
|------|-------|-------------|
| **R5** | MUST | Every `spec.components` entry MUST have a unique ID |
| **R6** | MUST | Every component `type` MUST exist in the published component catalog |
| **R7** | MUST | Component `props` MUST validate against catalog JSON Schema for that type |
| **R8** | MUST NOT | A component ID be referenced by `ref:` unless it exists in `spec.components` |
| **R9** | SHOULD | Unused components (defined but never referenced) SHOULD emit a warning |
## Template rules
| Rule | Level | Description |
|------|-------|-------------|
| **R10** | MUST | Every `spec.templates` entry MUST declare a valid `layout` from the catalog |
| **R11** | MUST | Slot names in `templates.*.slots` MUST be valid for the declared layout |
| **R12** | MUST | Zone names in `templates.*.zones` MUST be valid for `FlexibleLayout` |
| **R13** | MUST | Each template MUST declare either `slots` or `zones`, not both |
| **R14** | MUST | Inline slot `{ type: X }` types MUST exist in the component catalog |
| **R15** | MUST | Slot `{ ref: id }` MUST resolve to a component in `spec.components` |
## Route rules
| Rule | Level | Description |
|------|-------|-------------|
| **R16** | MUST | Every route `template` MUST reference a key in `spec.templates` |
| **R17** | MUST | Routes MUST be ordered most-specific-first (longer/more precise patterns before wildcards) |
| **R18** | MUST | Route slot overrides MUST use valid slot names for the target template's layout |
| **R19** | SHOULD | At least one route SHOULD match the site root or docs base path |
## Content and navigation rules
| Rule | Level | Description |
|------|-------|-------------|
| **R20** | MUST | If `content.docs.dir` is declared, the directory MUST exist at compile time |
| **R21** | MUST | If `content.docs.nav.mode` is `manual`, `navigation.tabs` MUST be non-empty |
| **R22** | SHOULD | If `content.docs.nav.mode` is `auto`, a content source MUST be declared |
Navigation model:
- **`navigation.tabs`** — top tab bar (Mintlify-style)
- **`content.docs.nav.mode`** — sidebar source: `auto` (filesystem scan) or `manual` (use `navigation.tabs` groups)
## File include rules
| Rule | Level | Description |
|------|-------|-------------|
| **R23** | MUST NOT | `$yaml:`, `$json:`, or `$text:` paths traverse outside the project root |
| **R24** | MUST | Included files MUST parse as valid YAML/JSON/text respectively |
| **R25** | MUST NOT | Circular file includes (A includes B includes A) |
## Theme rules
| Rule | Level | Description |
|------|-------|-------------|
| **R26** | MUST | If `theme.tokens` is declared, the referenced file MUST exist and validate against DTCG 2025.10 |
| **R27** | MUST NOT | Both `theme.preset` and `theme.tokens` be declared simultaneously |
## Renderer conformance (target — not yet implemented in PraisonAIUI)
These rules define the target behaviour for ADP-conformant renderers. See [implementation-status.md](./implementation-status.md).
| Rule | Level | Description |
|------|-------|-------------|
| **R28** | MUST | Renderers MUST resolve the active template from `route-manifest.json` at runtime |
| **R29** | MUST | Renderers MUST honour `content.docs.nav.mode` when building sidebar |
| **R30** | MUST | Renderers MUST apply ARIA roles from catalog for each rendered component |
---
## Validation pipeline order
```
1. Parse YAML/JSON
2. Resolve $yaml/$json/$text includes (R23–R25)
3. JSON Schema validate (application.schema.json)
4. Cross-ref rules R1–R27
5. Component props validate against catalog (R6–R7)
6. Emit runtime manifests
7. Runtime conformance R28–R30 (renderer responsibility)
```
## Error format
Validation failures MUST report:
```json
{
"rule": "R8",
"path": "spec.templates.docs.slots.header.ref",
"message": "Component ref 'missing_header' not found in spec.components",
"severity": "error"
}
```
Warnings (R9) use `"severity": "warning"`.
@@ -0,0 +1,427 @@
# Application Design Protocol (ADP) — Research Report
**Version:** 1.0 (draft)
**Date:** August 2026
**Scope:** UI, layout, navigation, theming — declarative application design from text manifests
**Status:** Research and recommendation; not yet a published standard
---
## 1. Executive summary
There is no single existing standard for **designing full application UI from a declarative text file**. Instead, the problem is addressed in fragments:
- **Docs-site generators** (Mintlify, PraisonAIUI, Hugo) declare navigation and layout in config files.
- **Form libraries** (JSON Forms, RJSF) declare layout trees from JSON Schema.
- **Agent UI protocols** (A2UI, AG-UI) declare streaming component surfaces for LLM-generated UI.
- **Catalog standards** (OpenUI, Backstage) declare component metadata without runtime layout.
- **Design token standards** (DTCG 2025.10) declare theming without structure.
**Recommendation:** Define an **Application Design Protocol (ADP)** that synthesises proven patterns from 46 surveyed standards into a UI-only manifest format.
PraisonAIUI already implements approximately 60% of this model. ADP formalises, extends, and publishes what exists today.
---
## 2. Problem statement
### 2.1 What we need
A protocol that lets a developer (or LLM) describe an application's visual design — pages, navigation, components, themes, routes — in **one or more text files**, without writing application code.
### 2.2 What exists today (fragmented)
| Need | Who solves it | Gap |
|------|---------------|-----|
| Docs site from config | Mintlify, PraisonAIUI, Docusaurus | No shared spec; Mintlify uses JSON, others use YAML or JS |
| Component catalog | OpenUI, A2UI | No layout or routing |
| Layout trees | JSON Forms uiSchema | Forms only, not app shells |
| Theming | DTCG, Material 3 | Tokens only, no pages |
| Agent-generated UI | A2UI | Runtime streaming, not human-authored static sites |
| Software metadata | Backstage | Catalog entities, not UI composition |
### 2.3 Design goals for ADP
1. **Human-readable** — editable without IDE plugins
2. **LLM-safe** — structural schema rejects unknown fields
3. **Portable** — same manifest renders on any ADP-compliant runtime
4. **Validatable** — fail fast at build time, not in the browser
5. **Extensible** — plugins add catalog entries, not inline code
6. **Accessible** — ARIA hints required in component catalog
---
## 3. Standards landscape — 46 protocols in 10 categories
Each entry describes **how UI/layout/navigation is declared** — the primary comparison dimension.
### Category A: Web platform baseline (5)
| Standard | Format | How UI/layout/navigation is declared | Schema | Versioning |
|----------|--------|--------------------------------------|--------|------------|
| **HTML + CSS** | `.html` + `.css` | Semantic elements; CSS Flexbox/Grid; `<nav>` + links | Living spec grammar | Browser Baseline |
| **Web Components + DSD** | HTML + JS | Custom element tree; slots; Declarative Shadow DOM | DOM IDL | Platform flags |
| **WAI-ARIA + APG** | HTML attributes | Landmarks, widget roles, keyboard patterns | WAI-ARIA 1.2 | Rec track |
| **DTCG Design Tokens** | JSON | Token groups: `$value`, `$type`, `{alias}` refs — **no layout** | DTCG 2025.10 | Date editions |
| **OpenUI** | YAML/JSON | Component catalog: `components{id: props}` — **no layout tree** | Prop definitions | Per-library version |
### Category B: Schema-driven forms and layout (3)
| Standard | Format | How UI/layout/navigation is declared | Schema | Versioning |
|----------|--------|--------------------------------------|--------|------------|
| **JSON Forms** | JSON Schema + UI Schema | `VerticalLayout`, `Control`, `Categorization` with `scope` pointers | JSON Schema + uiSchema | Package SemVer |
| **RJSF** | JSON Schema + uiSchema | Schema-tree mirroring; `ui:widget`, `ui:options` | React convention | Package SemVer |
| **Google A2UI** | JSONL stream | Flat adjacency-list; `createSurface`; catalog-constrained types | JSON Schema catalog | v0.9.1 / v1.0 RC |
### Category C: Docs-site generators (6)
| Standard | Format | How UI/layout/navigation is declared | Schema | Versioning |
|----------|--------|--------------------------------------|--------|------------|
| **Mintlify** | JSON (`docs.json`) | `navigation.groups[].pages[]`; `$schema` URL; `$ref` splitting | Published JSON Schema URL | Platform |
| **PraisonAIUI** | YAML | `components` → `templates{layout, slots, zones}` → `routes` | Pydantic (internal) | `schemaVersion: 1` |
| **Docusaurus** | JS config | `themeConfig.navbar`, plugin sidebar config | TypeScript types | Package SemVer |
| **Hugo** | TOML/YAML | `menu.main[]`, theme selection | Hugo schema | Release train |
| **Jekyll** | YAML | Frontmatter per page; `_data/` nav | Conventions | SemVer |
| **Gatsby** | JS/TS | `siteMetadata` + `plugins[]` | JS object | Major versions |
### Category D: Native declarative UI — reference only (5)
| Standard | Format | Serialised wire format? | Navigation model |
|----------|--------|-------------------------|------------------|
| **Jetpack Compose** | Kotlin | No | `NavHost` + routes |
| **SwiftUI** | Swift | No | `NavigationStack` |
| **Flutter** | Dart | No | `Router` / `Navigator` |
| **Qt QML** | `.qml` | Yes | `StackView`, bindings |
| **XAML** | XML | Yes (dialect-fragmented) | `Frame`, `NavigationView` |
### Category E: Design systems (2)
| Standard | Format | Theming model |
|----------|--------|---------------|
| **Material Design 3** | DTCG JSON | reference → system → component token tiers |
| **Bootstrap 5.3** | SCSS → CSS vars | Sass maps → `--bs-*` custom properties |
### Category F: Design-to-code bridges — not source-of-truth (3)
| Standard | Output | Why not ADP source |
|----------|--------|-------------------|
| **Figma MCP** | React+Tailwind context | Proprietary; bridge only |
| **Webflow export** | HTML/CSS/JS ZIP | No schema; paid export |
| **Framer export** | React bundles | Platform lock-in |
### Category G: Component documentation (1)
| Standard | Format | Navigation |
|----------|--------|------------|
| **Storybook CSF 3** | TS modules | Sidebar via `title` path hierarchy |
### Category H: Configuration and packaging (12)
| Standard | Format | Composition mechanism | ADP borrow |
|----------|--------|----------------------|------------|
| **Kubernetes CRD** | YAML | `apiVersion/kind/spec`; structural OpenAPI | Structural validation |
| **Helm** | YAML + templates | Subcharts; values merge; `values.schema.json` | Defaults + overrides |
| **Backstage catalog** | YAML | `$yaml:`/`$json:` includes; relation graph | Envelope + includes |
| **OpenAPI 3.1** | YAML/JSON | `$ref`, `components` registry | JSON Schema foundation |
| **AsyncAPI 3.x** | YAML/JSON | Channels → messages | Future event UI |
| **JSON Schema 2020-12** | JSON | `$ref`, `$defs`, conditionals | Validation dialect |
| **CloudEvents** | JSON envelope | Fixed envelope + opaque payload | Future event wrapper |
| **Docker Compose** | YAML | Multi-file merge; profiles | Override stacking |
| **CUE** | CUE | Unify schema + data | Power-user compile layer |
| **Jsonnet** | Jsonnet → JSON | `import` + merge | Theme variants |
| **Pkl** | `.pkl` | `amends`, mixins | Typed config model |
| **OCI Artifact** | JSON manifest | Multi-layer blobs | Bundle packaging |
### Category I: Agent and workflow protocols (7)
| Standard | Format | UI/state declaration | ADP borrow |
|----------|--------|---------------------|------------|
| **MCP** | JSON-RPC | Tool schemas — no UI layout | Future widget actions |
| **AG-UI** | Streaming JSON | EventType enum; snapshots + deltas | Streaming model |
| **A2A** | Proto → JSON-RPC | Agent Cards | Transport |
| **AWP** | Multi-file YAML | 7 layers; rules R1–R32 | Layer + validation model |
| **LangGraph** | workflow.yaml | Graph nodes + edges | Layout graph |
| **AutoGen** | ComponentModel JSON | `{provider, type, version, config}` | Pluggable components |
| **CrewAI** | JSONC/YAML | Agent arrays — weak schema | Anti-pattern |
### Category J: Application packaging (2)
| Standard | Format | Application declaration |
|----------|--------|------------------------|
| **OAM** | YAML | `Application` with `components[]`, `traits[]` |
| **CNAB** | bundle.json | Parameters, credentials, actions |
---
## 4. How they do it — mechanism comparison matrix
| Mechanism | Standards | How it works | ADP |
|-----------|-----------|--------------|-----|
| **Envelope document** | K8s, Backstage, OAM, AWP | `apiVersion` + `kind` + `metadata` + `spec` | Adopt |
| **Component registry + ref** | PraisonAIUI, OpenUI | Named map; templates use `ref:` | Adopt |
| **Layout + slots** | PraisonAIUI, JSON Forms | Layout type + named regions | Adopt (unified) |
| **Route → template** | PraisonAIUI, Flutter | URL glob → template | Adopt |
| **Navigation tree** | Mintlify, Storybook | Hierarchical groups → pages | Adopt |
| **Design token file** | DTCG, Material 3 | Token JSON with aliases | Adopt |
| **Catalog allowlist** | A2UI, OpenUI | Closed component vocabulary | Adopt |
| **Schema + uiSchema split** | JSON Forms, RJSF | Data vs presentation | Partial |
| **Streaming surfaces** | A2UI, AG-UI | Incremental JSON updates | Future |
| **Layered spec** | AWP, Helm | Separate files merged at compile | Adopt |
| **File includes** | Backstage, Mintlify | `$yaml:` / `$ref` external files | Adopt |
| **Structural pruning** | K8s CRD | Reject unknown fields | Adopt |
| **Validation rules R1–Rn** | AWP | Cross-ref beyond JSON Schema | Adopt |
| **JS executable config** | Docusaurus, Gatsby | Code config | Reject |
| **Visual canvas** | Figma, Webflow | Proprietary designer | Reject |
| **Code-only UI** | Compose, SwiftUI | No wire format | Out of scope |
---
## 5. Side-by-side: four closest analogues
| Dimension | PraisonAIUI | Mintlify | A2UI | Backstage |
|-----------|-------------|----------|------|-----------|
| **Authoring format** | YAML | JSON | JSON (stream) | YAML |
| **Envelope** | `schemaVersion` only | Flat JSON | Message `version` | `apiVersion/kind/metadata/spec` |
| **Navigation** | `navigation.tabs[]` + filesystem | `navigation.groups[]` | N/A | N/A (metadata catalog) |
| **Components** | `components{id: type, props}` | N/A | Catalog + adjacency list | Entity kinds |
| **Layout** | `templates{layout, slots, zones}` | Theme only | Surfaces | N/A |
| **Routes** | `routes{match, template}` | Page paths in nav | N/A | N/A |
| **Schema published** | Pydantic only | `$schema` URL | JSON Schema catalog | JSON Schema per kind |
| **Runtime** | React SPA | Hosted platform | Any renderer | Catalog API |
| **Primary use** | Docs sites | Docs sites | Agent-generated UI | Software inventory |
**Synthesis:** ADP combines PraisonAIUI's composition model, Mintlify's schema publishing, A2UI's catalog allowlist, and Backstage's envelope and file includes.
---
## 6. YAML vs alternatives — structured debate
### 6.1 The assumption
YAML is the best authoring format for application design manifests.
### 6.2 Counter-evidence
**Mintlify** — the closest commercial docs-site analogue — uses **JSON** (`docs.json`) with a published `$schema` URL for IDE autocomplete. This works well for flat-to-moderately-nested config but becomes verbose for deep layout trees.
**A2UI** uses **JSON** as both authoring and wire format because LLMs emit structured JSON reliably and ambiguity is unacceptable at runtime.
**Docusaurus and Gatsby** use **JavaScript/TypeScript** config for programmatic power — but this sacrifices portability and LLM generation safety.
### 6.3 Format comparison
| Format | Strengths | Weaknesses for ADP |
|--------|-----------|-------------------|
| **YAML** | Comments; readable nesting; LLM-friendly; PraisonAIUI continuity | Ambiguous typing; merge key footguns; no native DRY |
| **JSON** | Unambiguous; `$schema` IDE support; Mintlify-proven | No comments; verbose for layout trees |
| **TOML** | Clear flat tables (Hugo) | Poor for nested templates/zones |
| **CUE / Jsonnet / Pkl** | Typed composition without templating | Learning curve; not LLM-default |
| **HCL** | Modules, expressions | Ecosystem-bound; overkill for UI-only |
| **JS/TS** | Full programmatic power | Not portable; not LLM-safe |
### 6.4 Recommendation
```
Author (human/LLM) → YAML (default) or JSON (alternate)
↓
ADP compiler/validator (JSON Schema 2020-12)
↓
Normalised JSON manifest (runtime)
↓
UI renderer (catalog-constrained)
```
- **Primary authoring:** YAML — best for nested `templates` / `zones` trees
- **Alternate authoring:** JSON — for teams preferring Mintlify-style `$schema` tooling
- **Never at runtime:** YAML — always compile to JSON before serve
- **Composition:** `$yaml:` file includes (Backstage), not Helm Go templates
- **Power users:** Optional CUE/Jsonnet compiles to YAML before validation
**Verdict:** YAML remains the recommended default, but ADP MUST accept JSON as an equivalent authoring format with identical semantics.
---
## 7. ADP design principles
1. **Declarative, not executable** — manifests describe intent; runtimes render; no inline code or templating logic in core spec.
2. **Schema-first** — JSON Schema 2020-12 is canonical; YAML is authoring convenience.
3. **Structural strictness** — unknown fields rejected in strict mode (K8s CRD pattern).
4. **Catalog allowlist** — component `type` must exist in published catalog (A2UI pattern).
5. **Envelope consistency** — every document uses `apiVersion`, `kind`, `metadata`, `spec`.
6. **Separation of concerns** — theme, navigation, components, templates, routes are distinct spec sections.
7. **Compile, don't interpret** — YAML → validated JSON manifest → static assets at build time.
8. **Accessibility by default** — catalog entries include ARIA role hints (WAI-ARIA pattern).
9. **Versioned protocol** — `schemaVersion` integer + `apiVersion` string; breaking changes bump version.
10. **Extension namespace** — `x-adp-*` prefix reserved; plugins register catalog layers.
11. **File composition** — `$yaml:` includes for modular configs (Backstage pattern).
12. **Route-driven rendering** — URL resolution selects template; no hardcoded layout in renderer.
---
## 8. Proposed ADP document model
```yaml
schemaVersion: 1
apiVersion: adp.io/v1alpha1
kind: Application
metadata:
name: my-docs-site
version: "1.0.0"
labels: {}
spec:
site:
title: "My Site"
description: "..."
theme:
preset: zinc # or DTCG token ref
radius: md
darkMode: true
navigation:
mode: auto # auto | manual
tabs: [...]
components:
header_main:
type: Header
props: { logoText: "..." }
templates:
docs:
layout: ThreeColumnLayout
slots:
header: { ref: header_main }
main: { type: DocContent }
routes:
- match: "/docs/**"
template: docs
content:
docs:
dir: "./docs"
```
### Data flow
```
application.yaml → Parse → JSON Schema validate → Cross-ref rules (R1–R30)
→ Compile → ui-config.json + route-manifest.json + docs-nav.json
→ React / static renderer
```
---
## 9. PraisonAIUI alignment and gaps
### Already aligned
| PraisonAIUI today | ADP equivalent |
|-------------------|----------------|
| `schemaVersion: 1` | `schemaVersion` + `apiVersion` |
| `components` + `ref:` | `spec.components` + slot refs |
| `templates.layout` + `slots` | `spec.templates` |
| `routes` | `spec.routes` |
| `site.theme` | `spec.theme` |
| Compiler → JSON manifests | ADP compile pipeline |
| Pydantic validation | JSON Schema + cross-ref rules |
### Gaps to close
| Gap | ADP fix |
|-----|---------|
| No published JSON Schema | Publish `application.schema.json` |
| `navigation.tabs` compiled but not rendered | Mandate runtime consumption in spec |
| `route-manifest.json` not used for routing | Mandate route-driven template resolution |
| Dual slots + zones models | Unified: zones = ordered slot arrays |
| Closed hardcoded component types | Published `component-catalog.schema.json` |
| No envelope (`kind`, `metadata`) | Add Backstage-style envelope |
---
## 10. Migration path
Existing `aiui.template.yaml` files remain valid. Migration steps:
1. Add `apiVersion: adp.io/v1alpha1` and `kind: Application` (optional during transition).
2. Wrap top-level keys under `spec:` (compiler accepts both shapes during v1alpha1).
3. Rename `site` fields unchanged; map `navigation`, `templates`, `routes` as-is.
4. Publish catalog; register custom component types via plugin schema layers.
See [adp/aiui-mapping.md](./adp/aiui-mapping.md) for field-by-field mapping.
---
## 11. Open questions
1. **Should ADP standardise layout type names** (`ThreeColumnLayout`) or allow runtime-defined layouts?
2. **Should navigation support both filesystem-auto and manual tabs** in one spec, or require a mode switch?
3. **Should agent-driven surfaces (A2UI) be a separate `kind: Surface`** or embedded in Application?
4. **Should theme reference DTCG token files directly** or use simplified preset shorthand?
---
## 12. Appendix A — Standards index
| # | Standard | URL |
|---|----------|-----|
| 1 | A2UI | https://a2ui.org/ |
| 2 | AG-UI | https://docs.ag-ui.com/ |
| 3 | AWP | https://github.com/veegee82/agent-workflow-protocol |
| 4 | AsyncAPI | https://www.asyncapi.com/ |
| 5 | Backstage catalog | https://backstage.io/docs/features/software-catalog/descriptor-format |
| 6 | Bootstrap | https://getbootstrap.com/ |
| 7 | CloudEvents | https://cloudevents.io/ |
| 8 | CNAB | https://cnab.io/ |
| 9 | CrewAI | https://docs.crewai.com/ |
| 10 | CUE | https://cuelang.org/ |
| 11 | Docusaurus | https://docusaurus.io/ |
| 12 | DTCG Design Tokens | https://www.designtokens.org/ |
| 13 | Figma Code Connect | https://developers.figma.com/docs/code-connect/ |
| 14 | Framer | https://www.framer.com/ |
| 15 | Gatsby | https://www.gatsbyjs.com/ |
| 16 | Helm | https://helm.sh/ |
| 17 | Hugo | https://gohugo.io/ |
| 18 | Jekyll | https://jekyllrb.com/ |
| 19 | JSON Forms | https://jsonforms.io/ |
| 20 | JSON Schema | https://json-schema.org/ |
| 21 | Kubernetes CRD | https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ |
| 22 | LangGraph | https://langchain-ai.github.io/langgraph/ |
| 23 | Material Design 3 | https://m3.material.io/ |
| 24 | MCP | https://modelcontextprotocol.io/ |
| 25 | Mintlify | https://mintlify.com/docs |
| 26 | OAM | https://oam.dev/ |
| 27 | OCI Artifact | https://opencontainers.org/ |
| 28 | OpenAPI | https://www.openapis.org/ |
| 29 | OpenUI | https://openuispec.org/ |
| 30 | Pkl | https://pkl-lang.org/ |
| 31 | PraisonAIUI | https://github.com/MervinPraison/PraisonAIUI |
| 32 | RJSF | https://rjsf-team.github.io/react-jsonschema-form/ |
| 33 | Storybook CSF | https://storybook.js.org/docs/api/csf |
| 34 | W3C ARIA APG | https://www.w3.org/WAI/ARIA/apg/ |
| 35 | Web Components | https://web.dev/articles/declarative-shadow-dom |
| 36 | Webflow | https://webflow.com/ |
| 37 | AutoGen | https://microsoft.github.io/autogen/ |
| 38 | A2A | https://google.github.io/A2A/ |
| 39 | Jsonnet | https://jsonnet.org/ |
| 40 | Docker Compose | https://docs.docker.com/compose/ |
| 41 | Qt QML | https://doc.qt.io/qt-6/qmlapplications.html |
| 42 | XAML | https://learn.microsoft.com/en-us/windows/apps/develop/platform/xaml/xaml-overview |
| 43 | Jetpack Compose | https://developer.android.com/develop/ui/compose |
| 44 | SwiftUI | https://developer.apple.com/xcode/swiftui/ |
| 45 | HTML + CSS | https://html.spec.whatwg.org/ |
| 46 | Flutter | https://flutter.dev/ |
---
## 13. Conclusion
ADP is feasible and largely prefigured by PraisonAIUI's existing architecture. The research across 46 standards yields a clear synthesis:
- **Envelope** from Backstage/K8s
- **Composition** from PraisonAIUI (components → templates → routes)
- **Catalog** from A2UI/OpenUI
- **Theming** from DTCG
- **Validation** from AWP (structural schema + R-rules)
- **Authoring** in YAML (default) with JSON alternate (Mintlify pattern)
**Status:** Research draft — see [adp/implementation-status.md](./adp/implementation-status.md) for implementation gaps.
The draft spec, JSON Schema files, and example manifests are published alongside this report in [`docs/protocols/adp/`](./adp/).
+45 -23
View File
@@ -2,6 +2,12 @@
> Each example teaches exactly **one new concept** — no overlap.
See also: [`docs/examples/basic.md`](../docs/examples/basic.md) and [`docs/examples/advanced.md`](../docs/examples/advanced.md).
**Note:** Some folders share numeric prefixes (`02-*`, `19-*`) — treat the path name as the canonical ID, not the number alone.
---
## Python Track
| # | Name | Unique Concept | Run |
@@ -15,16 +21,32 @@
| 07 | [provider-praisonai](python/07-provider-praisonai/) | Custom `BaseProvider` subclass, `RunEvent` protocol | `aiui run app.py` |
| 08 | [streaming](python/08-streaming/) | PraisonAI Agent streaming via `stream_emitter` | `aiui run app.py` |
| 09 | [widget](python/09-widget/) | Copilot/sidebar mode | `aiui run app.py` |
| 10 | [feature-showcase](python/10-feature-showcase/) | All protocol features seeded, `create_app()` | `python app.py` |
| 11 | [ui-integration](python/11-ui-integration/) | Gradio ASGI mount, Streamlit iframe, REST embedding | `python app.py` |
| 12 | [agent-dashboard](python/12-agent-dashboard/) | OpenClaw-style rendered HTML admin panel | `python app.py` |
| 10 | [feature-showcase](python/10-feature-showcase/) | All protocol features seeded, `create_app()` | `aiui run app.py` |
| 11 | [ui-integration](python/11-ui-integration/) | Gradio ASGI mount, Streamlit iframe, REST embedding | `aiui run app.py` |
| 12 | ~~agent-dashboard~~ → Use [dashboard-test](python/15-dashboard-test/) | ~~Removed in v0.5.0 (XSS vulnerability)~~ | See example 15 |
| 13 | [real-dashboard](python/13-real-dashboard/) | Real PraisonAI Agent + native dashboard + live metrics | `aiui run app.py --style dashboard` |
| 19 | [email-bot](python/19-email-bot/) | `EmailBot` IMAP/SMTP + agent replies + subject commands | `python app.py` |
| 20 | [email-channel](python/20-email-channel/) | Email as AIUI dashboard channel via `/api/channels` | `python app.py` |
| 21 | [agentmail-bot](python/21-agentmail-bot/) | `AgentMailBot` API-first email + programmatic inbox lifecycle | `python app.py` |
| 22 | [agentmail-channel](python/22-agentmail-channel/) | AgentMail as AIUI dashboard channel via `/api/channels` | `python app.py` |
| 25 | [clean-chat](python/25-clean-chat/) | `set_dashboard(sidebar=False)` — dashboard chat without left nav | `aiui run app.py` |
| 26 | [custom-theme-chat](python/26-custom-theme-chat/) | `register_theme()` — dynamic custom theme (teal/cyan) | `aiui run app.py` |
| 14 | [all-features](python/14-all-features/) | Integration test suite for all 16 feature APIs | `python app.py` |
| 15 | [dashboard-test](python/15-dashboard-test/) | Protocol-first page registration, view registry | `python app.py` |
| 16 | [gateway-integration](python/16-gateway-integration/) | `AIUIGateway` — real agent execution + WebSocket streaming | `python app.py` |
| 17 | [three-column-demo](python/17-three-column-demo/) | Three-column feature explorer with gateway | `python app.py` |
| 18 | [full-chat](python/18-full-chat/) | `aiui.set_pages()` page curation + custom explorer page | `python app.py` |
| 19 | [components-showcase](python/19-components-showcase/) | All 54 UI component types in one dashboard | `aiui run app.py --style dashboard` |
| 20 | [email-bot](python/20-email-bot/) | `EmailBot` IMAP/SMTP + agent replies + subject commands | `python app.py` |
| 21 | [email-channel](python/21-email-channel/) | Email as AIUI dashboard channel via `/api/channels` | `aiui run app.py` |
| 22 | [agentmail-bot](python/22-agentmail-bot/) | `AgentMailBot` API-first email + programmatic inbox lifecycle | `python app.py` |
| 23 | [agentmail-channel](python/23-agentmail-channel/) | AgentMail as AIUI dashboard channel via `/api/channels` | `aiui run app.py` |
| 24 | [e2e-components-test](python/24-e2e-components-test/) | Components, docs, and chat rendering in one app | `python app.py` |
| 25 | [custom-design](python/25-custom-design/) | Every chat design knob (`set_theme`, branding, layout) | `aiui run app.py` |
| 26 | [clean-chat](python/26-clean-chat/) | `set_dashboard(sidebar=False)` — dashboard chat without left nav | `aiui run app.py` |
| 27 | [custom-theme-chat](python/27-custom-theme-chat/) | `register_theme()` — dynamic custom theme (teal/cyan) | `aiui run app.py` |
| 28 | [multica-style](python/28-multica-style/) | Multica-inspired UI features (glass, gradients, motion) | `python app.py` |
| 29 | [full-extensibility](python/29-full-extensibility/) | All extension points: pages, forms, `set_custom_js`, plugins | `python app.py` |
| 30 | [a2ui-canvas](python/30-a2ui-canvas/) | A2UI canvas + `send_a2ui_messages` integration | `python app.py` |
| 31 | [image-preview](python/31-image-preview/) | `ImageAgent` inline chat image preview via `MESSAGE_ELEMENT` | `aiui run app.py` |
| — | [external-agent-dashboard](python/external-agent-dashboard/) | Minimal external agent on dashboard shell | `aiui run app.py` |
| — | [platform-board](python/platform-board/) | Kanban board from PraisonAI Platform issues API | `aiui run app.py` |
| — | [praisonai-claw-board](python/praisonai-claw-board/) | Kanban board from `aiui` jobs store | `aiui run app.py` |
| — | [video-studio](python/video-studio/) | Video Studio — YAML scenes via Video engine sidecar | `aiui run app.py` |
---
@@ -33,22 +55,22 @@
| # | Name | Shows | Run |
|---|---|---|---|
| 01 | [chat](yaml/01-chat/) | Zero-code YAML chat agent | `aiui run chat.yaml` |
| 02 | [clean-chat](yaml/02-clean-chat/) | `dashboard.sidebar: false` — dashboard chat without left nav (YAML parity) | `aiui run clean-chat.yaml` |
| 02 | [clean-chat](yaml/02-clean-chat/) | `dashboard.sidebar: false` — dashboard chat without left nav | `aiui run clean-chat.yaml` |
| 02 | [profiles](yaml/02-profiles/) | YAML agent profiles / persona switching | `aiui run chat.yaml` |
| 03 | [tools](yaml/03-tools/) | Built-in YAML tool registration | `aiui run chat.yaml` |
| 04 | [features](yaml/04-features/) | Feature flags and dashboard options in YAML | `aiui run chat.yaml` |
| 12 | [playground](yaml/12-playground/) | YAML agent playground with multiple profiles | `aiui run chat.yaml --style agents` |
---
## YAML Track — Site Templates
| # | Name | Shows |
|---|---|---|
| 01 | minimal | 5-line YAML → live docs site |
| 02 | with-sidebar | `DocsSidebar` slot, directory-based navigation |
| 03 | with-toc | `Toc` slot, right-column table of contents |
| 04 | multi-section | Multiple content directories and grouped routes |
| 05 | custom-homepage | `Hero` + `FeatureList` slot components |
| 06 | fullstack | Header/Footer refs, CTA buttons, dark mode |
| 07 | seo-config | `<meta>` tags, `<title>` per-page, Open Graph |
| 08 | api-reference | `ApiSidebar`, route-level slot overrides |
| 09 | typescript-custom | Custom TypeScript component registration |
| 10 | i18n-ready | Multi-language content directories |
| 11 | monorepo | Workspaces, shared components, per-package docs |
| # | Name | Shows | Run |
|---|---|---|---|
| 05 | [minimal](yaml/05-minimal/) | 5-line YAML → live docs site | `aiui build --config aiui.template.yaml` |
| 06 | [blocks](yaml/06-blocks/) | shadcn component dependency installation | `aiui build --config aiui.template.yaml` |
| 07 | [colorful](yaml/07-colorful/) | Colourful marketing theme with hero zones | `aiui build --config aiui.template.yaml` |
| 08 | [corporate](yaml/08-corporate/) | Corporate docs with SEO, i18n, and a11y fields | `aiui build --config aiui.template.yaml` |
| 09 | [dark-modern](yaml/09-dark-modern/) | Dark-mode modern docs layout | `aiui build --config aiui.template.yaml` |
| 10 | [developer-portal](yaml/10-developer-portal/) | API reference sidebar and route overrides | `aiui build --config aiui.template.yaml` |
| 11 | [full-featured](yaml/11-full-featured/) | Full enterprise config (SEO, i18n, a11y, search) | `aiui build --config aiui.template.yaml` |
+12 -12
View File
@@ -40,8 +40,10 @@ def _get_client():
_client = AsyncOpenAI(api_key=api_key)
return _client
# Keep per-session context in memory (DataStore handles persistence)
_contexts: dict[str, list[dict]] = {}
# Use SessionGovernor for bounded session memory management
from praisonaiui.session_governor import SessionGovernor
_governor = SessionGovernor(max_turns=20, max_sessions=100)
@aiui.starters
@@ -66,20 +68,18 @@ async def on_message(message: str):
"""Send the message to OpenAI and stream the response."""
session_id = message.session_id if hasattr(message, "session_id") else "default"
# Build conversation context
if session_id not in _contexts:
_contexts[session_id] = [
{"role": "system", "content": "You are a helpful, concise assistant."}
]
_contexts[session_id].append({"role": "user", "content": str(message)})
# Add user message to bounded session context
_governor.add_message(session_id, {"role": "user", "content": str(message)})
await aiui.think("Thinking...")
# Get bounded context for LLM
messages = _governor.get_context(session_id)
# Stream from OpenAI
stream = await _get_client().chat.completions.create(
model="gpt-4o-mini",
messages=_contexts[session_id],
messages=messages,
stream=True,
)
@@ -90,8 +90,8 @@ async def on_message(message: str):
full_response += delta.content
await aiui.stream_token(delta.content)
# Save assistant response to context
_contexts[session_id].append({"role": "assistant", "content": full_response})
# Save assistant response to bounded context
_governor.add_message(session_id, {"role": "assistant", "content": full_response})
# Feedback buttons
await aiui.action_buttons([
@@ -15,8 +15,20 @@ Run:
"""
import asyncio
import sys
import praisonaiui as aiui
# Support --datastore json flag as documented
if "--datastore" in sys.argv:
try:
idx = sys.argv.index("--datastore")
if idx + 1 < len(sys.argv) and sys.argv[idx + 1] == "json":
from praisonaiui.datastore import JSONFileDataStore
aiui.set_datastore(JSONFileDataStore())
except (IndexError, ImportError):
pass
# Lazy agent — only created when first message is sent
_agent = None
+66 -31
View File
@@ -17,10 +17,10 @@ Run:
"""
import asyncio
import praisonaiui as aiui
from praisonaiui.provider import BaseProvider, RunEvent, RunEventType
# === Option 1: Provider-based (Direct Mode) ===
# Uses PraisonAI Agent directly with streaming bridge
@@ -65,13 +65,11 @@ class FullFeaturedProvider(BaseProvider):
def calculate(expression: str) -> str:
"""Evaluate a math expression safely."""
from praisonaiui.math_eval import eval_math_expression
try:
# Safe evaluation of basic math
allowed = set("0123456789+-*/.() ")
if all(c in allowed for c in expression):
result = eval(expression)
return f"Result: {result}"
return "Error: Only basic math operations allowed"
result = eval_math_expression(expression)
return f"Result: {result}"
except Exception as e:
return f"Error: {e}"
@@ -89,7 +87,8 @@ class FullFeaturedProvider(BaseProvider):
yield RunEvent(type=RunEventType.REASONING_STARTED)
yield RunEvent(type=RunEventType.REASONING_STEP, step="Analyzing the request...")
# Set up streaming bridge
# Get the running event loop for thread-safe queue operations
loop = asyncio.get_running_loop()
event_queue = asyncio.Queue()
_has_streaming = False
@@ -99,27 +98,32 @@ class FullFeaturedProvider(BaseProvider):
def _on_stream(evt):
if evt.type == SET.DELTA_TEXT and evt.content:
if evt.is_reasoning:
event_queue.put_nowait(RunEvent(
run_event = RunEvent(
type=RunEventType.REASONING_STEP,
step=evt.content,
))
)
# Use thread-safe queue operation
loop.call_soon_threadsafe(event_queue.put_nowait, run_event)
else:
event_queue.put_nowait(RunEvent(
run_event = RunEvent(
type=RunEventType.RUN_CONTENT,
token=evt.content,
))
)
loop.call_soon_threadsafe(event_queue.put_nowait, run_event)
elif evt.type == SET.DELTA_TOOL_CALL and evt.tool_call:
event_queue.put_nowait(RunEvent(
run_event = RunEvent(
type=RunEventType.TOOL_CALL_STARTED,
name=evt.tool_call.get("name"),
args=evt.tool_call.get("arguments"),
))
)
loop.call_soon_threadsafe(event_queue.put_nowait, run_event)
elif evt.type == SET.TOOL_CALL_END and evt.tool_call:
event_queue.put_nowait(RunEvent(
run_event = RunEvent(
type=RunEventType.TOOL_CALL_COMPLETED,
name=evt.tool_call.get("name"),
result=evt.tool_call.get("result"),
))
)
loop.call_soon_threadsafe(event_queue.put_nowait, run_event)
agent.stream_emitter.add_callback(_on_stream)
agent.stream_emitter.enable_metrics()
@@ -129,26 +133,57 @@ class FullFeaturedProvider(BaseProvider):
yield RunEvent(type=RunEventType.REASONING_COMPLETED)
# Run the agent
try:
response = await asyncio.to_thread(agent.chat, message)
full_response = str(response)
except Exception as exc:
yield RunEvent(type=RunEventType.RUN_ERROR, error=str(exc))
return
finally:
if _has_streaming:
# Run the agent concurrently with event draining
if _has_streaming:
async def _drain_events():
"""Drain events concurrently with agent execution."""
while True:
event = await event_queue.get()
if event is None:
break
yield event
chat_task = asyncio.create_task(asyncio.to_thread(agent.chat, message))
async def _signal_end():
try:
await chat_task
finally:
await event_queue.put(None)
signal_task = asyncio.create_task(_signal_end())
try:
# Drain events while agent is running
async for event in _drain_events():
yield event
# Wait for agent completion
response = await chat_task
full_response = str(response)
except Exception as exc:
yield RunEvent(type=RunEventType.RUN_ERROR, error=str(exc))
return
finally:
try:
agent.stream_emitter.remove_callback(_on_stream)
except Exception:
pass
# Drain streamed events
while not event_queue.empty():
# Ensure signal task is cleaned up
if not signal_task.done():
signal_task.cancel()
try:
await signal_task
except asyncio.CancelledError:
pass
else:
# Fallback without streaming
try:
yield event_queue.get_nowait()
except asyncio.QueueEmpty:
break
response = await asyncio.to_thread(agent.chat, message)
full_response = str(response)
except Exception as exc:
yield RunEvent(type=RunEventType.RUN_ERROR, error=str(exc))
return
# If no tokens were streamed, emit the full response
if event_queue.empty() and not _has_streaming:
+17 -27
View File
@@ -10,14 +10,17 @@ Run:
aiui run app.py
"""
import os
import asyncio
import os
import sys
import praisonaiui as aiui
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '_shared'))
from stream_bridge import StreamBridge
try:
from praisonaiagents import Agent
from praisonaiagents.streaming.events import StreamEventType
except ImportError:
raise ImportError(
"praisonaiagents package required. Install with: pip install praisonaiagents"
@@ -70,36 +73,22 @@ async def on_message(message: str):
await aiui.think("Thinking...")
# Set up a streaming callback that forwards tokens to PraisonAIUI
token_queue = asyncio.Queue()
# Set up thread-safe streaming bridge
bridge = StreamBridge()
def on_stream_event(event):
"""Callback invoked by the Agent's stream_emitter for each token."""
if event.type == StreamEventType.DELTA_TEXT and event.content:
token_queue.put_nowait(event.content)
elif event.type == StreamEventType.FIRST_TOKEN and event.content:
token_queue.put_nowait(event.content)
elif event.type == StreamEventType.STREAM_END:
token_queue.put_nowait(None) # Sentinel
# Register callback
agent.stream_emitter.add_callback(on_stream_event)
# Register callback using the bridge
callback = bridge.emitter_callback()
agent.stream_emitter.add_callback(callback)
agent.stream_emitter.enable_metrics()
try:
# Run agent.chat in a thread (it's synchronous)
chat_task = asyncio.get_event_loop().run_in_executor(
None, lambda: agent.chat(str(message), stream=True)
# Run agent.chat concurrently with token consumption
chat_task = asyncio.create_task(
asyncio.to_thread(lambda: agent.chat(str(message), stream=True))
)
# Drain tokens as they arrive and stream to UI
while True:
try:
token = await asyncio.wait_for(token_queue.get(), timeout=60.0)
except asyncio.TimeoutError:
break
if token is None: # Sentinel from STREAM_END
break
# Consume tokens as they arrive and stream to UI
async for token in bridge.consume():
await aiui.stream_token(token)
# Wait for the chat to fully complete
@@ -107,7 +96,8 @@ async def on_message(message: str):
finally:
# Clean up callback
agent.stream_emitter.remove_callback(on_stream_event)
agent.stream_emitter.remove_callback(callback)
bridge.cancel()
@aiui.cancel
+8 -4
View File
@@ -11,18 +11,21 @@ Demonstrates:
Run:
pip install praisonaiui
python app.py
aiui run app.py
# Dashboard at http://localhost:8082
# Set HOST=0.0.0.0 for Docker/LAN demos (defaults to 127.0.0.1)
"""
import sys
import os
import sys
import uvicorn
from praisonaiui.server import create_app
import uvicorn
# Use shared seed data helper
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from _shared.console import safe_print as print
from _shared.seed_data import seed_demo_data
seed_demo_data()
@@ -31,4 +34,5 @@ if __name__ == "__main__":
app = create_app()
print("✅ Full Dashboard at http://localhost:8082")
print(" API: http://localhost:8082/api/features")
uvicorn.run(app, host="0.0.0.0", port=8082, log_level="info")
host = os.getenv("HOST", "127.0.0.1")
uvicorn.run(app, host=host, port=8082, log_level="info")
+3 -1
View File
@@ -20,6 +20,7 @@ import sys
# Use shared seed data helper
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from _shared.console import safe_print as print
from _shared.seed_data import seed_demo_data
@@ -171,7 +172,8 @@ def run_standalone():
aiui_app.routes.insert(0, Route("/", landing, methods=["GET"]))
print("✅ UI Integration Demo at http://localhost:8082")
uvicorn.run(aiui_app, host="0.0.0.0", port=8082, log_level="info")
host = os.getenv("HOST", "127.0.0.1")
uvicorn.run(aiui_app, host=host, port=8082, log_level="info")
if __name__ == "__main__":
-493
View File
@@ -1,493 +0,0 @@
"""Agent Dashboard — OpenClaw/agent-ui style admin panel.
What's New (vs full-dashboard/):
• Rendered HTML dashboard — not just JSON APIs
• Sidebar navigation with live page switching
• Status badges, toggle buttons, real-time data
• Glassmorphism dark theme
• All data from PraisonAIUI feature APIs
Run:
PYTHONPATH=src python app.py
# Dashboard at http://localhost:8082
"""
import os
import sys
import uvicorn
from starlette.responses import HTMLResponse
from starlette.routing import Route
from praisonaiui.server import create_app
# Use shared seed data helper
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from _shared.seed_data import seed_demo_data
# ── HTML Template ────────────────────────────────────────────────────
DASHBOARD_HTML = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agent Dashboard — PraisonAIUI</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-primary: #0a0a12;
--bg-card: #12121f;
--bg-hover: #1a1a2e;
--border: #2a2a4a;
--text-primary: #e0e0e8;
--text-secondary: #94a3b8;
--accent-purple: #7c3aed;
--accent-cyan: #06b6d4;
--accent-green: #22c55e;
--accent-amber: #f59e0b;
--accent-red: #ef4444;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', -apple-system, sans-serif; background: var(--bg-primary);
color: var(--text-primary); display: flex; min-height: 100vh; }
/* Sidebar */
.sidebar { width: 240px; background: var(--bg-card); border-right: 1px solid var(--border);
display: flex; flex-direction: column; position: fixed; height: 100vh; z-index: 10; }
.sidebar-header { padding: 1.25rem; border-bottom: 1px solid var(--border); }
.sidebar-header h1 { font-size: 1.1rem; font-weight: 700;
background: linear-gradient(90deg, var(--accent-purple), var(--accent-cyan));
-webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.sidebar-header p { font-size: 0.7rem; color: var(--text-secondary); margin-top: 2px; }
.nav-group { padding: 0.75rem 0; }
.nav-group-label { font-size: 0.65rem; color: var(--text-secondary); text-transform: uppercase;
letter-spacing: 0.1em; padding: 0 1rem; margin-bottom: 0.25rem; font-weight: 600; }
.nav-item { display: flex; align-items: center; gap: 0.6rem; padding: 0.5rem 1rem;
cursor: pointer; transition: all 0.2s; font-size: 0.85rem; color: var(--text-secondary);
border-left: 3px solid transparent; }
.nav-item:hover { background: var(--bg-hover); color: var(--text-primary); }
.nav-item.active { background: var(--bg-hover); color: var(--accent-purple);
border-left-color: var(--accent-purple); font-weight: 500; }
.nav-icon { font-size: 1rem; width: 1.5rem; text-align: center; }
.nav-badge { margin-left: auto; background: var(--accent-purple); color: white;
font-size: 0.65rem; padding: 1px 6px; border-radius: 10px; }
/* Main content */
.main { margin-left: 240px; flex: 1; padding: 1.5rem; }
.page-header { margin-bottom: 1.5rem; }
.page-header h2 { font-size: 1.5rem; font-weight: 600; }
.page-header p { color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem; }
/* Cards grid */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem; margin-bottom: 1.5rem; }
.stat-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px;
padding: 1.25rem; transition: border-color 0.3s; }
.stat-card:hover { border-color: var(--accent-purple); }
.stat-label { font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase;
letter-spacing: 0.05em; }
.stat-value { font-size: 1.75rem; font-weight: 700; margin-top: 0.25rem;
background: linear-gradient(90deg, var(--accent-purple), var(--accent-cyan));
-webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.stat-sub { font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.25rem; }
/* Table */
.data-table { width: 100%; background: var(--bg-card); border: 1px solid var(--border);
border-radius: 10px; overflow: hidden; }
.data-table th { text-align: left; padding: 0.75rem 1rem; font-size: 0.75rem;
text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-secondary);
background: var(--bg-hover); border-bottom: 1px solid var(--border); font-weight: 600; }
.data-table td { padding: 0.75rem 1rem; font-size: 0.85rem; border-bottom: 1px solid var(--border); }
.data-table tr:last-child td { border-bottom: none; }
.data-table tr:hover td { background: rgba(124, 58, 237, 0.05); }
/* Badges */
.badge { padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: 600; }
.badge-online { background: rgba(34, 197, 94, 0.15); color: var(--accent-green); }
.badge-offline { background: rgba(239, 68, 68, 0.15); color: var(--accent-red); }
.badge-enabled { background: rgba(34, 197, 94, 0.15); color: var(--accent-green); }
.badge-disabled { background: rgba(148, 163, 184, 0.15); color: var(--text-secondary); }
.badge-running { background: rgba(6, 182, 212, 0.15); color: var(--accent-cyan); }
.badge-platform { background: rgba(124, 58, 237, 0.1); color: var(--accent-purple); }
/* Toggle switch */
.toggle { position: relative; width: 36px; height: 20px; cursor: pointer; }
.toggle input { display: none; }
.toggle-slider { position: absolute; inset: 0; background: #333; border-radius: 10px;
transition: 0.3s; }
.toggle-slider::before { content: ''; position: absolute; left: 2px; top: 2px; width: 16px;
height: 16px; background: white; border-radius: 50%; transition: 0.3s; }
.toggle input:checked + .toggle-slider { background: var(--accent-purple); }
.toggle input:checked + .toggle-slider::before { transform: translateX(16px); }
/* Section */
.section { margin-bottom: 1.5rem; }
.section-title { font-size: 0.85rem; color: var(--text-secondary); font-weight: 600;
text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.75rem;
display: flex; align-items: center; gap: 0.5rem; }
.section-count { background: var(--accent-purple); color: white; font-size: 0.65rem;
padding: 1px 6px; border-radius: 10px; }
/* Page display */
.page { display: none; }
.page.active { display: block; }
/* Animations */
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); } }
.page.active { animation: fadeIn 0.3s ease; }
/* Feature list */
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem; }
.feature-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px;
padding: 1.25rem; transition: all 0.3s; }
.feature-card:hover { border-color: var(--accent-purple); transform: translateY(-2px); }
.feature-name { font-weight: 600; margin-bottom: 0.25rem; }
.feature-desc { font-size: 0.8rem; color: var(--text-secondary); }
.feature-routes { margin-top: 0.75rem; font-size: 0.75rem; color: var(--text-secondary); }
.feature-route { font-family: monospace; font-size: 0.7rem; color: var(--accent-cyan);
display: block; margin-top: 2px; }
/* Empty state */
.empty { text-align: center; padding: 3rem; color: var(--text-secondary); }
.empty-icon { font-size: 3rem; margin-bottom: 1rem; }
</style>
</head>
<body>
<aside class="sidebar">
<div class="sidebar-header">
<h1>🤖 Agent Dashboard</h1>
<p>PraisonAIUI Admin</p>
</div>
<nav>
<div class="nav-group">
<div class="nav-group-label">Overview</div>
<div class="nav-item active" data-page="overview">
<span class="nav-icon">📊</span> Overview
</div>
<div class="nav-item" data-page="features">
<span class="nav-icon">⚡</span> Features
<span class="nav-badge" id="nav-feature-count">—</span>
</div>
</div>
<div class="nav-group">
<div class="nav-group-label">Messaging</div>
<div class="nav-item" data-page="channels">
<span class="nav-icon">📡</span> Channels
<span class="nav-badge" id="nav-channel-count">—</span>
</div>
</div>
<div class="nav-group">
<div class="nav-group-label">Infrastructure</div>
<div class="nav-item" data-page="nodes">
<span class="nav-icon">🖥️</span> Nodes
<span class="nav-badge" id="nav-node-count">—</span>
</div>
<div class="nav-item" data-page="instances">
<span class="nav-icon">📻</span> Instances
<span class="nav-badge" id="nav-instance-count">—</span>
</div>
</div>
<div class="nav-group">
<div class="nav-group-label">Automation</div>
<div class="nav-item" data-page="schedules">
<span class="nav-icon">⏰</span> Schedules
<span class="nav-badge" id="nav-schedule-count">—</span>
</div>
<div class="nav-item" data-page="skills">
<span class="nav-icon">🧩</span> Skills
<span class="nav-badge" id="nav-skill-count">—</span>
</div>
</div>
</nav>
</aside>
<main class="main">
<!-- OVERVIEW PAGE -->
<div class="page active" id="page-overview">
<div class="page-header">
<h2>Overview</h2>
<p>Real-time system status at a glance</p>
</div>
<div class="stats-grid" id="overview-stats"></div>
</div>
<!-- FEATURES PAGE -->
<div class="page" id="page-features">
<div class="page-header">
<h2>Features</h2>
<p>All registered protocol feature modules</p>
</div>
<div class="feature-grid" id="features-grid"></div>
</div>
<!-- CHANNELS PAGE -->
<div class="page" id="page-channels">
<div class="page-header">
<h2>Channels</h2>
<p>Multi-platform messaging channels</p>
</div>
<div class="section">
<div class="section-title">Active Channels <span class="section-count" id="channel-count">0</span></div>
<table class="data-table" id="channels-table">
<thead><tr>
<th>Name</th><th>Platform</th><th>Status</th><th>Enabled</th><th>Last Activity</th>
</tr></thead>
<tbody></tbody>
</table>
</div>
</div>
<!-- NODES PAGE -->
<div class="page" id="page-nodes">
<div class="page-header">
<h2>Nodes</h2>
<p>Execution nodes and agent bindings</p>
</div>
<div class="section">
<div class="section-title">Registered Nodes <span class="section-count" id="node-count">0</span></div>
<table class="data-table" id="nodes-table">
<thead><tr>
<th>Name</th><th>Host</th><th>Platform</th><th>Status</th><th>Agents</th><th>Policy</th>
</tr></thead>
<tbody></tbody>
</table>
</div>
</div>
<!-- INSTANCES PAGE -->
<div class="page" id="page-instances">
<div class="page-header">
<h2>Instances</h2>
<p>Connected clients and workers</p>
</div>
<div class="section">
<div class="section-title">Connected Instances <span class="section-count" id="instance-count">0</span></div>
<table class="data-table" id="instances-table">
<thead><tr>
<th>ID</th><th>Host</th><th>Platform</th><th>Mode</th><th>Roles</th><th>Last Seen</th>
</tr></thead>
<tbody></tbody>
</table>
</div>
</div>
<!-- SCHEDULES PAGE -->
<div class="page" id="page-schedules">
<div class="page-header">
<h2>Schedules</h2>
<p>Cron jobs and scheduled tasks</p>
</div>
<div class="section">
<div class="section-title">Scheduled Jobs <span class="section-count" id="schedule-count">0</span></div>
<table class="data-table" id="schedules-table">
<thead><tr>
<th>Name</th><th>Schedule</th><th>Agent</th><th>Enabled</th><th>Runs</th><th>Last Run</th>
</tr></thead>
<tbody></tbody>
</table>
</div>
</div>
<!-- SKILLS PAGE -->
<div class="page" id="page-skills">
<div class="page-header">
<h2>Skills</h2>
<p>Registered agent capabilities</p>
</div>
<div class="section">
<div class="section-title">Agent Skills <span class="section-count" id="skill-count">0</span></div>
<table class="data-table" id="skills-table">
<thead><tr>
<th>Name</th><th>ID</th><th>Enabled</th><th>Config</th>
</tr></thead>
<tbody></tbody>
</table>
</div>
</div>
</main>
<script>
// ── Navigation ──────────────────────────────────────────────────────
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
item.classList.add('active');
document.getElementById('page-' + item.dataset.page).classList.add('active');
});
});
// ── Data loading ────────────────────────────────────────────────────
async function fetchJSON(url) {
const r = await fetch(url);
return r.json();
}
function timeAgo(ts) {
if (!ts) return '—';
const s = Math.floor(Date.now() / 1000 - ts);
if (s < 60) return s + 's ago';
if (s < 3600) return Math.floor(s / 60) + 'm ago';
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
return Math.floor(s / 86400) + 'd ago';
}
async function loadOverview() {
const [features, channels, nodes, instances, schedules, skills] = await Promise.all([
fetchJSON('/api/features'),
fetchJSON('/api/channels'),
fetchJSON('/api/nodes'),
fetchJSON('/api/instances'),
fetchJSON('/api/schedules'),
fetchJSON('/api/skills'),
]);
// Nav badges
document.getElementById('nav-feature-count').textContent = features.count;
document.getElementById('nav-channel-count').textContent = channels.count;
document.getElementById('nav-node-count').textContent = nodes.count;
document.getElementById('nav-instance-count').textContent = instances.count;
document.getElementById('nav-schedule-count').textContent = schedules.count;
document.getElementById('nav-skill-count').textContent = skills.count;
// Overview stats
const statsEl = document.getElementById('overview-stats');
const runningChannels = channels.channels.filter(c => c.running).length;
const onlineNodes = nodes.nodes.filter(n => n.status === 'online').length;
statsEl.innerHTML = `
<div class="stat-card">
<div class="stat-label">Features</div>
<div class="stat-value">${features.count}</div>
<div class="stat-sub">Protocol modules active</div>
</div>
<div class="stat-card">
<div class="stat-label">Channels</div>
<div class="stat-value">${channels.count}</div>
<div class="stat-sub">${runningChannels} running</div>
</div>
<div class="stat-card">
<div class="stat-label">Nodes</div>
<div class="stat-value">${nodes.count}</div>
<div class="stat-sub">${onlineNodes} online</div>
</div>
<div class="stat-card">
<div class="stat-label">Instances</div>
<div class="stat-value">${instances.count}</div>
<div class="stat-sub">Connected clients & workers</div>
</div>
<div class="stat-card">
<div class="stat-label">Schedules</div>
<div class="stat-value">${schedules.count}</div>
<div class="stat-sub">Cron jobs registered</div>
</div>
<div class="stat-card">
<div class="stat-label">Skills</div>
<div class="stat-value">${skills.count}</div>
<div class="stat-sub">Agent capabilities</div>
</div>
`;
// Features grid
const fg = document.getElementById('features-grid');
fg.innerHTML = features.features.map(f => `
<div class="feature-card">
<div class="feature-name">${f.name}</div>
<div class="feature-desc">${f.description}</div>
<div class="feature-routes">${f.routes.length} routes
${f.routes.slice(0, 3).map(r => '<span class="feature-route">' + r + '</span>').join('')}
${f.routes.length > 3 ? '<span class="feature-route">... +' + (f.routes.length - 3) + ' more</span>' : ''}
</div>
</div>
`).join('');
// Channels table
document.getElementById('channel-count').textContent = channels.count;
document.querySelector('#channels-table tbody').innerHTML = channels.channels.map(c => `
<tr>
<td><strong>${c.name}</strong></td>
<td><span class="badge badge-platform">${c.platform}</span></td>
<td>${c.running ? '<span class="badge badge-running">● running</span>' : '<span class="badge badge-offline">● stopped</span>'}</td>
<td>${c.enabled ? '<span class="badge badge-enabled">enabled</span>' : '<span class="badge badge-disabled">disabled</span>'}</td>
<td>${timeAgo(c.last_activity)}</td>
</tr>
`).join('');
// Nodes table
document.getElementById('node-count').textContent = nodes.count;
document.querySelector('#nodes-table tbody').innerHTML = nodes.nodes.map(n => `
<tr>
<td><strong>${n.name}</strong></td>
<td><code>${n.host}</code></td>
<td>${n.platform}</td>
<td>${n.status === 'online' ? '<span class="badge badge-online">● online</span>' : '<span class="badge badge-offline">● offline</span>'}</td>
<td>${(n.agents || []).map(a => '<span class="badge badge-platform">' + a + '</span> ').join('')}</td>
<td><span class="badge">${n.approval_policy}</span></td>
</tr>
`).join('');
// Instances table
document.getElementById('instance-count').textContent = instances.count;
document.querySelector('#instances-table tbody').innerHTML = instances.instances.map(i => `
<tr>
<td><code>${i.id}</code></td>
<td>${i.host}</td>
<td>${i.platform}</td>
<td><span class="badge badge-${i.mode === 'worker' ? 'running' : 'enabled'}">${i.mode}</span></td>
<td>${(i.roles || []).join(', ')}</td>
<td>${timeAgo(i.last_seen)}</td>
</tr>
`).join('');
// Schedules table
document.getElementById('schedule-count').textContent = schedules.count;
document.querySelector('#schedules-table tbody').innerHTML = schedules.schedules.map(s => `
<tr>
<td><strong>${s.name}</strong></td>
<td><code>${s.schedule}</code></td>
<td>${s.agent_id || '—'}</td>
<td>${s.enabled ? '<span class="badge badge-enabled">enabled</span>' : '<span class="badge badge-disabled">disabled</span>'}</td>
<td>${s.run_count || 0}</td>
<td>${timeAgo(s.last_run)}</td>
</tr>
`).join('');
// Skills table
document.getElementById('skill-count').textContent = skills.count;
document.querySelector('#skills-table tbody').innerHTML = skills.skills.map(s => `
<tr>
<td><strong>${s.name}</strong></td>
<td><code>${s.id}</code></td>
<td>${s.enabled ? '<span class="badge badge-enabled">enabled</span>' : '<span class="badge badge-disabled">disabled</span>'}</td>
<td><code>${JSON.stringify(s.config || {})}</code></td>
</tr>
`).join('');
}
// Auto-refresh every 10 seconds
loadOverview();
setInterval(loadOverview, 10000);
</script>
</body>
</html>"""
# ── Entry point ──────────────────────────────────────────────────────
seed_demo_data()
if __name__ == "__main__":
app = create_app()
# Add the dashboard HTML landing page
async def dashboard_landing(request):
return HTMLResponse(DASHBOARD_HTML)
app.routes.insert(0, Route("/", dashboard_landing, methods=["GET"]))
print("✅ Agent Dashboard at http://localhost:8082")
print(" JSON API: http://localhost:8082/api/features")
uvicorn.run(app, host="0.0.0.0", port=8082, log_level="info")
+19 -24
View File
@@ -1,6 +1,6 @@
"""Real Agent Dashboard — PraisonAI agents + PraisonAIUI dashboard.
Unlike 12-agent-dashboard (mock data), this runs REAL PraisonAI agents.
This example uses secure component-based rendering with REAL PraisonAI agents.
Uses PraisonAIUI's native dashboard with @reply, @page, register_agent()
— all backed by actual LLM calls.
@@ -18,15 +18,19 @@ Run:
aiui run app.py --style dashboard
"""
import os
import time
import asyncio
import os
import sys
import time
import praisonaiui as aiui
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '_shared'))
from console import safe_print as print
from stream_bridge import StreamBridge
try:
from praisonaiagents import Agent
from praisonaiagents.streaming.events import StreamEventType
except ImportError:
raise ImportError(
"praisonaiagents required: pip install praisonaiagents"
@@ -102,31 +106,21 @@ async def on_message(message: str):
await aiui.think("Agent thinking...")
start = time.time()
token_queue = asyncio.Queue()
def on_stream(event):
if event.type == StreamEventType.DELTA_TEXT and event.content:
token_queue.put_nowait(event.content)
elif event.type == StreamEventType.FIRST_TOKEN and event.content:
token_queue.put_nowait(event.content)
elif event.type == StreamEventType.STREAM_END:
token_queue.put_nowait(None)
agent.stream_emitter.add_callback(on_stream)
# Set up thread-safe streaming bridge
bridge = StreamBridge()
callback = bridge.emitter_callback()
agent.stream_emitter.add_callback(callback)
full = []
try:
task = asyncio.get_event_loop().run_in_executor(
None, lambda: agent.chat(str(message), stream=True)
# Run agent.chat concurrently with token consumption
task = asyncio.create_task(
asyncio.to_thread(lambda: agent.chat(str(message), stream=True))
)
while True:
try:
tok = await asyncio.wait_for(token_queue.get(), timeout=60)
except asyncio.TimeoutError:
break
if tok is None:
break
# Consume tokens as they arrive and stream to UI
async for tok in bridge.consume():
full.append(tok)
await aiui.stream_token(tok)
@@ -139,7 +133,8 @@ async def on_message(message: str):
await aiui.say(f"❌ Error: {e}")
finally:
agent.stream_emitter.remove_callback(on_stream)
agent.stream_emitter.remove_callback(callback)
bridge.cancel()
# ── @page — live metrics from real calls ─────────────────────────────
+5 -6
View File
@@ -1,19 +1,18 @@
"""Example 14: All Features Demo — Comprehensive API test suite.
Demonstrates ALL 16 PraisonAIUI features with real API calls.
Run: PYTHONPATH=src python examples/python/14-all-features/app.py
Run: aiui run app.py
"""
import sys
import os
# Add paths for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "src"))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".."))
import sys
from starlette.testclient import TestClient
from praisonaiui.server import create_app
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from _shared.console import safe_print as print
def test_all_features():
"""Test all 16 feature APIs with real data."""
+3 -1
View File
@@ -85,5 +85,7 @@ async def metrics_page():
app = create_app()
if __name__ == "__main__":
import os
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8082)
host = os.getenv("HOST", "127.0.0.1")
uvicorn.run(app, host=host, port=8082)
+19 -15
View File
@@ -44,15 +44,18 @@ import sys
# ── Import praisonaiui ──────────────────────────────────────
import praisonaiui as aiui
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from _shared.console import icon, safe_print
# ── Check gateway availability ──────────────────────────────
try:
from praisonaiui.integration import AIUIGateway
from praisonaiagents import Agent
GATEWAY_OK = True
except ImportError as e:
print(f"⚠️ Gateway dependencies missing: {e}")
print(" Install with: pip install praisonai praisonaiagents")
print(" Falling back to standalone mode.")
safe_print(f"{icon('⚠️', '[WARN]')} Gateway dependencies missing: {e}")
safe_print(" Install with: pip install praisonai praisonaiagents")
safe_print(" Falling back to standalone mode.")
GATEWAY_OK = False
# ── Set dashboard style ─────────────────────────────────────
@@ -149,18 +152,19 @@ def _register_agents_in_dashboard():
"model": agent_def["model"],
"icon": agent_def["icon"],
})
print(f" ✓ Dashboard: {agent_def['icon']} {agent_def['name']}")
safe_print(f" {icon('✓', '[OK]')} Dashboard: {agent_def['name']}")
# ── Main ────────────────────────────────────────────────────
async def main():
host = os.getenv("HOST", "127.0.0.1")
if GATEWAY_OK:
print("🚀 Starting with Gateway mode (full execution + streaming)")
print()
safe_print(f"{icon('🚀', '[START]')} Starting with Gateway mode (full execution + streaming)")
safe_print()
# Create the gateway
gateway = AIUIGateway(
host="0.0.0.0",
host=host,
port=8083,
)
@@ -173,29 +177,29 @@ async def main():
self_reflect=False,
)
gateway.register_agent(agent, agent_id=agent_def["agent_id"])
print(f" ✓ Gateway: {agent_def['icon']} {agent_def['name']} ({agent_def['model']})")
safe_print(f" {icon('✓', '[OK]')} Gateway: {agent_def['name']} ({agent_def['model']})")
# ── Register agents in dashboard CRUD storage ───────
_register_agents_in_dashboard()
print()
print(f" Dashboard: http://localhost:8083")
print(f" WebSocket: ws://localhost:8083/ws")
print(f" Inspector: http://localhost:8083 → 🔍 Feature Inspector")
print()
safe_print()
safe_print(" Dashboard: http://localhost:8083")
safe_print(" WebSocket: ws://localhost:8083/ws")
safe_print(f" Inspector: http://localhost:8083 {icon('→', '->')} {icon('🔍', '')} Feature Inspector")
safe_print()
# Start the gateway — this runs the full server
await gateway.start()
else:
# Fallback to standalone mode (same as example 15)
print("📦 Starting in standalone mode (CRUD only, no agent execution)")
safe_print(f"{icon('📦', '[INFO]')} Starting in standalone mode (CRUD only, no agent execution)")
from praisonaiui.server import create_app
import uvicorn
_register_agents_in_dashboard()
app = create_app()
config = uvicorn.Config(app, host="0.0.0.0", port=8083, log_level="info")
config = uvicorn.Config(app, host=host, port=8083, log_level="info")
server = uvicorn.Server(config)
await server.serve()
+24 -20
View File
@@ -26,15 +26,18 @@ import sys
import praisonaiui as aiui
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from _shared.console import icon, safe_print
# ── Check gateway availability ──────────────────────────────
try:
from praisonaiui.integration import AIUIGateway
from praisonaiagents import Agent
GATEWAY_OK = True
except ImportError as e:
print(f"⚠️ Gateway dependencies not found: {e}")
print(" Install with: pip install praisonai praisonaiagents")
print(" Falling back to standalone mode (REST APIs only).\n")
safe_print(f"{icon('⚠️', '[WARN]')} Gateway dependencies not found: {e}")
safe_print(" Install with: pip install praisonai praisonaiagents")
safe_print(" Falling back to standalone mode (REST APIs only).\n")
GATEWAY_OK = False
aiui.set_style("dashboard")
@@ -115,19 +118,20 @@ def _register_agents_in_dashboard():
"model": agent_def["model"],
"icon": agent_def["icon"],
})
print(f" ✓ Agent: {agent_def['icon']} {agent_def['name']}")
safe_print(f" {icon('✓', '[OK]')} Agent: {agent_def['name']}")
# ── Main ────────────────────────────────────────────────────
async def main():
port = int(os.getenv("PORT", "8082"))
host = os.getenv("HOST", "127.0.0.1")
if GATEWAY_OK:
print("🚀 Starting with Gateway mode (full execution + streaming)")
print()
safe_print(f"{icon('🚀', '[START]')} Starting with Gateway mode (full execution + streaming)")
safe_print()
gateway = AIUIGateway(host="0.0.0.0", port=port)
gateway = AIUIGateway(host=host, port=port)
# Register agents with gateway for real execution
for agent_def in AGENTS:
@@ -138,34 +142,34 @@ async def main():
reflection=False,
)
gateway.register_agent(agent, agent_id=agent_def["agent_id"])
print(f" ✓ Gateway: {agent_def['icon']} {agent_def['name']} ({agent_def['model']})")
safe_print(f" {icon('✓', '[OK]')} Gateway: {agent_def['name']} ({agent_def['model']})")
# Also register in dashboard CRUD
_register_agents_in_dashboard()
print()
print(f" Dashboard: http://localhost:{port}")
print(f" Explorer: http://localhost:{port} → 🔬 Feature Explorer")
print(f" WebSocket: ws://localhost:{port}/ws")
print()
safe_print()
safe_print(f" Dashboard: http://localhost:{port}")
safe_print(f" Explorer: http://localhost:{port} {icon('→', '->')} {icon('🔬', '')} Feature Explorer")
safe_print(f" WebSocket: ws://localhost:{port}/ws")
safe_print()
await gateway.start()
else:
# Standalone: REST APIs only, no agent execution
print("📦 Starting in standalone mode (REST APIs only, no agent execution)")
safe_print(f"{icon('📦', '[INFO]')} Starting in standalone mode (REST APIs only, no agent execution)")
from praisonaiui.server import create_app
import uvicorn
_register_agents_in_dashboard()
app = create_app()
print()
print(f" Dashboard: http://localhost:{port}")
print(f" Explorer: http://localhost:{port} → 🔬 Feature Explorer")
print(f" Note: POST endpoints may fail without gateway")
print()
safe_print()
safe_print(f" Dashboard: http://localhost:{port}")
safe_print(f" Explorer: http://localhost:{port} {icon('→', '->')} {icon('🔬', '')} Feature Explorer")
safe_print(" Note: POST endpoints may fail without gateway")
safe_print()
config = uvicorn.Config(app, host="0.0.0.0", port=port, log_level="info")
config = uvicorn.Config(app, host=host, port=port, log_level="info")
server = uvicorn.Server(config)
await server.serve()
+42 -24
View File
@@ -1,10 +1,13 @@
"""PraisonAIUI — Full Chat Example (with Gateway + Page Config).
Dashboard with curated sidebar pages and a custom Feature Explorer:
- Uses ``aiui.set_pages()`` to show only essential built-in pages
Clean chat-focused dashboard with curated sidebar pages:
- Uses ``aiui.set_pages()`` to show only essential pages (chat, sessions, agents, usage, config)
- Hides debug/admin pages for a streamlined experience
- Adds a custom "Feature Explorer" page via ``@aiui.page()``
- Registers three agents: Researcher, Writer, Coder
Compare with 17-three-column-demo which shows ALL available pages.
Modes:
1. Gateway mode (recommended) — starts real agent execution + WebSocket
Requires: pip install praisonai praisonaiagents
@@ -26,19 +29,32 @@ import sys
import praisonaiui as aiui
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from _shared.console import icon, safe_print
# ── Check gateway availability ──────────────────────────────
try:
from praisonaiui.integration import AIUIGateway
from praisonaiagents import Agent
from praisonaiui.integration import AIUIGateway
GATEWAY_OK = True
except ImportError as e:
print(f"⚠️ Gateway dependencies not found: {e}")
print(" Install with: pip install praisonai praisonaiagents")
print(" Falling back to standalone mode (REST APIs only).\n")
safe_print(f"{icon('⚠️', '[WARN]')} Gateway dependencies not found: {e}")
safe_print(" Install with: pip install praisonai praisonaiagents")
safe_print(" Falling back to standalone mode (REST APIs only).\n")
GATEWAY_OK = False
aiui.set_style("dashboard")
# ── Page curation ───────────────────────────────────────────
aiui.set_pages([
"chat", # Main chat interface
"sessions", # Session management
"agents", # Agent configuration
"usage", # Usage metrics
"config", # Configuration settings
])
# ── Agent definitions ───────────────────────────────────────
AGENTS = [
@@ -115,19 +131,20 @@ def _register_agents_in_dashboard():
"model": agent_def["model"],
"icon": agent_def["icon"],
})
print(f" ✓ Agent: {agent_def['icon']} {agent_def['name']}")
safe_print(f" {icon('✓', '[OK]')} Agent: {agent_def['name']}")
# ── Main ────────────────────────────────────────────────────
async def main():
port = int(os.getenv("PORT", "8082"))
host = os.getenv("HOST", "127.0.0.1")
if GATEWAY_OK:
print("🚀 Starting with Gateway mode (full execution + streaming)")
print()
safe_print(f"{icon('🚀', '[START]')} Starting with Gateway mode (full execution + streaming)")
safe_print()
gateway = AIUIGateway(host="0.0.0.0", port=port)
gateway = AIUIGateway(host=host, port=port)
# Register agents with gateway for real execution
for agent_def in AGENTS:
@@ -138,34 +155,35 @@ async def main():
reflection=False,
)
gateway.register_agent(agent, agent_id=agent_def["agent_id"])
print(f" ✓ Gateway: {agent_def['icon']} {agent_def['name']} ({agent_def['model']})")
safe_print(f" {icon('✓', '[OK]')} Gateway: {agent_def['name']} ({agent_def['model']})")
# Also register in dashboard CRUD
_register_agents_in_dashboard()
print()
print(f" Dashboard: http://localhost:{port}")
print(f" Explorer: http://localhost:{port} → 🔬 Feature Explorer")
print(f" WebSocket: ws://localhost:{port}/ws")
print()
safe_print()
safe_print(f" Dashboard: http://localhost:{port}")
safe_print(f" Explorer: http://localhost:{port} {icon('→', '->')} {icon('🔬', '')} Feature Explorer")
safe_print(f" WebSocket: ws://localhost:{port}/ws")
safe_print()
await gateway.start()
else:
# Standalone: REST APIs only, no agent execution
print("📦 Starting in standalone mode (REST APIs only, no agent execution)")
from praisonaiui.server import create_app
safe_print(f"{icon('📦', '[INFO]')} Starting in standalone mode (REST APIs only, no agent execution)")
import uvicorn
from praisonaiui.server import create_app
_register_agents_in_dashboard()
app = create_app()
print()
print(f" Dashboard: http://localhost:{port}")
print(f" Explorer: http://localhost:{port} → 🔬 Feature Explorer")
print(f" Note: POST endpoints may fail without gateway")
print()
safe_print()
safe_print(f" Dashboard: http://localhost:{port}")
safe_print(f" Explorer: http://localhost:{port} {icon('→', '->')} {icon('🔬', '')} Feature Explorer")
safe_print(" Note: POST endpoints may fail without gateway")
safe_print()
config = uvicorn.Config(app, host="0.0.0.0", port=port, log_level="info")
config = uvicorn.Config(app, host=host, port=port, log_level="info")
server = uvicorn.Server(config)
await server.serve()
@@ -32,6 +32,9 @@ import asyncio
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from _shared.console import icon, safe_print
# ── Check prerequisites ─────────────────────────────────────────────
try:
@@ -40,8 +43,8 @@ try:
SDK_OK = True
except ImportError:
SDK_OK = False
print("⚠️ Missing dependencies. Install with:")
print(" pip install praisonai[email] praisonaiagents")
safe_print(f"{icon('⚠️', '[WARN]')} Missing dependencies. Install with:")
safe_print(" pip install praisonai[email] praisonaiagents")
sys.exit(1)
@@ -68,8 +71,8 @@ agent = Agent(
async def run_simple():
"""Start email bot with automatic env var resolution."""
bot = Bot("email", agent=agent)
print(f"📧 Starting email bot for: {os.getenv('EMAIL_ADDRESS', '(not set)')}")
print(" Listening for new emails... (Ctrl+C to stop)")
safe_print(f"{icon('📧', '[EMAIL]')} Starting email bot for: {os.getenv('EMAIL_ADDRESS', '(not set)')}")
safe_print(" Listening for new emails... (Ctrl+C to stop)")
await bot.start()
@@ -96,9 +99,9 @@ async def run_with_handlers():
@bot.on_message
async def log_email(message):
subject = message.metadata.get("subject", "(no subject)")
print(f"📨 From: {message.sender.username}")
print(f" Subject: {subject}")
print(f" Preview: {message.text[:100]}...")
safe_print(f"{icon('📨', '[MSG]')} From: {message.sender.username}")
safe_print(f" Subject: {subject}")
safe_print(f" Preview: {message.text[:100]}...")
# Custom command: emails with subject starting with "/status"
@bot.on_command("status")
@@ -118,11 +121,11 @@ async def run_with_handlers():
reply_to=message.message_id,
)
print(f"📧 Starting email bot with custom handlers")
print(f" Email: {bot._email_address}")
print(f" Poll interval: {config.polling_interval}s")
print(f" Allowed senders: {'all' if not config.allowed_users else config.allowed_users}")
print(" Listening... (Ctrl+C to stop)")
safe_print(f"{icon('📧', '[EMAIL]')} Starting email bot with custom handlers")
safe_print(f" Email: {bot._email_address}")
safe_print(f" Poll interval: {config.polling_interval}s")
safe_print(f" Allowed senders: {'all' if not config.allowed_users else config.allowed_users}")
safe_print(" Listening... (Ctrl+C to stop)")
await bot.start()
@@ -131,17 +134,17 @@ async def run_with_handlers():
async def run_probe():
"""Test email server connectivity."""
bot = Bot("email", agent=agent)
print("🔍 Testing email server connectivity...")
safe_print(f"{icon('🔍', '[PROBE]')} Testing email server connectivity...")
result = await bot.probe()
print(f"\n Connection: {'✅ OK' if result.ok else '❌ FAILED'}")
print(f" Platform: {result.platform}")
safe_print(f"\n Connection: {icon('✅', '[OK]') + ' OK' if result.ok else icon('❌', '[FAIL]') + ' FAILED'}")
safe_print(f" Platform: {result.platform}")
if result.bot_username:
print(f" Email: {result.bot_username}")
safe_print(f" Email: {result.bot_username}")
if result.elapsed_ms:
print(f" Latency: {result.elapsed_ms:.0f}ms")
safe_print(f" Latency: {result.elapsed_ms:.0f}ms")
if result.error:
print(f" Error: {result.error}")
safe_print(f" Error: {result.error}")
# ── Main ────────────────────────────────────────────────────────────
@@ -31,7 +31,7 @@ Requires:
export EMAIL_APP_PASSWORD=your_app_password
Run:
python app.py
aiui run app.py
# Dashboard at http://localhost:8084
"""
@@ -42,7 +42,9 @@ import sys
# ── Imports ─────────────────────────────────────────────────────────
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "src"))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from _shared.console import icon, safe_print
from praisonaiui.server import create_app
import uvicorn
@@ -85,16 +87,17 @@ def seed_email_channel():
"email_address": os.getenv("EMAIL_ADDRESS", "[email protected]"),
"imap_server": os.getenv("EMAIL_IMAP_SERVER", "imap.gmail.com"),
"smtp_server": os.getenv("EMAIL_SMTP_SERVER", "smtp.gmail.com"),
"app_password_ref": "env:EMAIL_APP_PASSWORD",
"polling_interval": 30,
},
})
if r.status_code in (200, 201):
channel_id = r.json().get("id", "unknown")
print(f" ✅ Email channel registered: {channel_id}")
safe_print(f" {icon('✅', '[OK]')} Email channel registered: {channel_id}")
else:
print(f" ⚠️ Channel registration: {r.status_code} — {r.json()}")
safe_print(f" {icon('⚠️', '[WARN]')} Channel registration: {r.status_code} - {r.json()}")
else:
print(" ✓ Email channel already exists")
safe_print(f" {icon('✓', '[OK]')} Email channel already exists")
# Also seed other channels for comparison in dashboard
for name, platform in [("Discord #general", "discord"), ("Telegram Support", "telegram")]:
@@ -105,7 +108,7 @@ def seed_email_channel():
"platform": platform,
"config": {},
})
print(f" ✓ Seeded {platform} channel")
safe_print(f" {icon('✓', '[OK]')} Seeded {platform} channel")
# ── Verify email platform support ──────────────────────────────────
@@ -121,8 +124,8 @@ def verify_email_support():
if r.status_code == 200:
platforms = r.json().get("platforms", [])
email_supported = "email" in platforms
print(f" Supported platforms: {platforms}")
print(f" Email supported: {'✅ Yes' if email_supported else '❌ No — add email to SUPPORTED_PLATFORMS'}")
safe_print(f" Supported platforms: {platforms}")
safe_print(f" Email supported: {icon('✅', '[OK]') + ' Yes' if email_supported else icon('❌', '[X]') + ' No - add email to SUPPORTED_PLATFORMS'}")
return email_supported
return False
@@ -136,50 +139,51 @@ def test_channel_api():
app = create_app()
client = TestClient(app)
print("\n📋 Channel API Test")
print("=" * 40)
safe_print(f"\n{icon('📋', '[TEST]')} Channel API Test")
safe_print("=" * 40)
# List channels
r = client.get("/api/channels")
channels = r.json().get("channels", [])
print(f" GET /api/channels: {len(channels)} channels")
safe_print(f" GET /api/channels: {len(channels)} channels")
for ch in channels:
icon = "📧" if ch.get("platform") == "email" else "💬"
print(f" {icon} {ch.get('name', '?')} ({ch.get('platform', '?')})")
ch_icon = icon("📧", "[email]") if ch.get("platform") == "email" else icon("💬", "[chat]")
safe_print(f" {ch_icon} {ch.get('name', '?')} ({ch.get('platform', '?')})")
# Get email channel status
email_ch = next((ch for ch in channels if ch.get("platform") == "email"), None)
if email_ch:
ch_id = email_ch.get("id")
r = client.get(f"/api/channels/{ch_id}/status")
print(f" GET /api/channels/{ch_id}/status: {r.json()}")
safe_print(f" GET /api/channels/{ch_id}/status: {r.json()}")
print()
safe_print()
# ── Main ────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("📧 PraisonAIUI — Email Channel Integration")
print("=" * 50)
print()
safe_print(f"{icon('📧', '[EMAIL]')} PraisonAIUI - Email Channel Integration")
safe_print("=" * 50)
safe_print()
if "--test" in sys.argv:
# Test mode: verify APIs + seed data
print("🔍 Verifying email platform support...")
safe_print(f"{icon('🔍', '[VERIFY]')} Verifying email platform support...")
verify_email_support()
print()
print("🌱 Seeding email channel...")
safe_print()
safe_print(f"{icon('🌱', '[SEED]')} Seeding email channel...")
seed_email_channel()
test_channel_api()
else:
# Run the dashboard
print("🌱 Seeding channels...")
safe_print(f"{icon('🌱', '[SEED]')} Seeding channels...")
seed_email_channel()
print()
safe_print()
app = create_app()
print(f"✅ Dashboard at http://localhost:8084")
print(f" Channels: http://localhost:8084/api/channels")
print(f" Platforms: http://localhost:8084/api/channels/platforms")
uvicorn.run(app, host="0.0.0.0", port=8084, log_level="info")
safe_print(f"{icon('✅', '[OK]')} Dashboard at http://localhost:8084")
safe_print(" Channels: http://localhost:8084/api/channels")
safe_print(" Platforms: http://localhost:8084/api/channels/platforms")
host = os.getenv("HOST", "127.0.0.1")
uvicorn.run(app, host=host, port=8084, log_level="info")
@@ -33,6 +33,9 @@ import asyncio
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from _shared.console import icon, safe_print
# ── Check prerequisites ─────────────────────────────────────────────
try:
@@ -41,8 +44,8 @@ try:
SDK_OK = True
except ImportError:
SDK_OK = False
print("⚠️ Missing dependencies. Install with:")
print(" pip install praisonai agentmail praisonaiagents")
safe_print(f"{icon('⚠️', '[WARN]')} Missing dependencies. Install with:")
safe_print(" pip install praisonai agentmail praisonaiagents")
sys.exit(1)
@@ -69,9 +72,9 @@ agent = Agent(
async def run_simple():
"""Start agentmail bot with automatic env var resolution."""
bot = Bot("agentmail", agent=agent)
print(f"📩 Starting AgentMail bot")
print(f" API key: {'✅ set' if os.getenv('AGENTMAIL_API_KEY') else '❌ not set'}")
print(" Listening for new emails... (Ctrl+C to stop)")
safe_print(f"{icon('📩', '[MAIL]')} Starting AgentMail bot")
safe_print(f" API key: {icon('✅', '[OK]') + ' set' if os.getenv('AGENTMAIL_API_KEY') else icon('❌', '[X]') + ' not set'}")
safe_print(" Listening for new emails... (Ctrl+C to stop)")
await bot.start()
@@ -99,9 +102,9 @@ async def run_with_handlers():
@bot.on_message
async def log_email(message):
subject = message.metadata.get("subject", "(no subject)")
print(f"📨 From: {message.sender.username}")
print(f" Subject: {subject}")
print(f" Preview: {message.text[:100]}...")
safe_print(f"{icon('📨', '[MSG]')} From: {message.sender.username}")
safe_print(f" Subject: {subject}")
safe_print(f" Preview: {message.text[:100]}...")
# Custom command: emails with subject starting with "/status"
@bot.on_command("status")
@@ -121,10 +124,10 @@ async def run_with_handlers():
reply_to=message.message_id,
)
print(f"📩 Starting AgentMail bot with custom handlers")
print(f" Email: {bot.email_address or '(will be assigned on start)'}")
print(f" Poll interval: {config.polling_interval}s")
print(" Listening... (Ctrl+C to stop)")
safe_print(f"{icon('📩', '[MAIL]')} Starting AgentMail bot with custom handlers")
safe_print(f" Email: {bot.email_address or '(will be assigned on start)'}")
safe_print(f" Poll interval: {config.polling_interval}s")
safe_print(" Listening... (Ctrl+C to stop)")
await bot.start()
@@ -136,30 +139,30 @@ async def run_inbox_demo():
token=os.environ.get("AGENTMAIL_API_KEY", ""),
)
print("📩 AgentMail Inbox Lifecycle Demo")
print("=" * 50)
safe_print(f"{icon('📩', '[MAIL]')} AgentMail Inbox Lifecycle Demo")
safe_print("=" * 50)
# Create inbox
print("\n1. Creating inbox...")
safe_print("\n1. Creating inbox...")
inbox = await bot.create_inbox(domain=os.getenv("AGENTMAIL_DOMAIN"))
print(f" ✅ Created: {inbox.get('email_address', 'unknown')}")
print(f" ID: {inbox.get('id', 'unknown')}")
safe_print(f" {icon('✅', '[OK]')} Created: {inbox.get('email_address', 'unknown')}")
safe_print(f" ID: {inbox.get('id', 'unknown')}")
# List inboxes
print("\n2. Listing inboxes...")
safe_print("\n2. Listing inboxes...")
inboxes = await bot.list_inboxes()
print(f" Found: {len(inboxes)} inbox(es)")
safe_print(f" Found: {len(inboxes)} inbox(es)")
for ib in inboxes:
print(f" 📧 {ib.get('email_address', '?')} (id: {ib.get('id', '?')})")
safe_print(f" {icon('📧', '[EMAIL]')} {ib.get('email_address', '?')} (id: {ib.get('id', '?')})")
# Delete inbox
inbox_id = inbox.get("id", "")
if inbox_id:
print(f"\n3. Deleting inbox {inbox_id}...")
safe_print(f"\n3. Deleting inbox {inbox_id}...")
deleted = await bot.delete_inbox(inbox_id)
print(f" {'✅ Deleted' if deleted else '❌ Failed to delete'}")
safe_print(f" {icon('✅', '[OK]') + ' Deleted' if deleted else icon('❌', '[X]') + ' Failed to delete'}")
print("\n✅ Inbox lifecycle complete")
safe_print(f"\n{icon('✅', '[OK]')} Inbox lifecycle complete")
# ── Probe: Test connectivity ────────────────────────────────────────
@@ -167,17 +170,17 @@ async def run_inbox_demo():
async def run_probe():
"""Test AgentMail API connectivity."""
bot = Bot("agentmail", agent=agent)
print("🔍 Testing AgentMail API connectivity...")
safe_print(f"{icon('🔍', '[PROBE]')} Testing AgentMail API connectivity...")
result = await bot.probe()
print(f"\n Connection: {'✅ OK' if result.ok else '❌ FAILED'}")
print(f" Platform: {result.platform}")
safe_print(f"\n Connection: {icon('✅', '[OK]') + ' OK' if result.ok else icon('❌', '[FAIL]') + ' FAILED'}")
safe_print(f" Platform: {result.platform}")
if result.bot_username:
print(f" Email: {result.bot_username}")
safe_print(f" Email: {result.bot_username}")
if result.elapsed_ms:
print(f" Latency: {result.elapsed_ms:.0f}ms")
safe_print(f" Latency: {result.elapsed_ms:.0f}ms")
if result.error:
print(f" Error: {result.error}")
safe_print(f" Error: {result.error}")
# ── Main ────────────────────────────────────────────────────────────
@@ -40,7 +40,9 @@ import sys
# ── Imports ─────────────────────────────────────────────────────────
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "src"))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from _shared.console import icon, safe_print
from praisonaiui.server import create_app
import uvicorn
@@ -80,7 +82,7 @@ def seed_agentmail_channel():
"name": "Agent Inbox",
"platform": "agentmail",
"config": {
"api_key": os.getenv("AGENTMAIL_API_KEY", ""),
"api_key_ref": "env:AGENTMAIL_API_KEY",
"domain": os.getenv("AGENTMAIL_DOMAIN", ""),
"inbox_id": os.getenv("AGENTMAIL_INBOX_ID", ""),
"polling_interval": 15,
@@ -88,11 +90,11 @@ def seed_agentmail_channel():
})
if r.status_code in (200, 201):
channel_id = r.json().get("id", "unknown")
print(f" ✅ AgentMail channel registered: {channel_id}")
safe_print(f" {icon('✅', '[OK]')} AgentMail channel registered: {channel_id}")
else:
print(f" ⚠️ Channel registration: {r.status_code} — {r.json()}")
safe_print(f" {icon('⚠️', '[WARN]')} Channel registration: {r.status_code} - {r.json()}")
else:
print(" ✓ AgentMail channel already exists")
safe_print(f" {icon('✓', '[OK]')} AgentMail channel already exists")
# Also seed email + telegram for comparison in dashboard
for name, platform in [("Support Email", "email"), ("Telegram Support", "telegram")]:
@@ -103,7 +105,7 @@ def seed_agentmail_channel():
"platform": platform,
"config": {},
})
print(f" ✓ Seeded {platform} channel")
safe_print(f" {icon('✓', '[OK]')} Seeded {platform} channel")
# ── Verify agentmail platform support ──────────────────────────────
@@ -120,9 +122,9 @@ def verify_agentmail_support():
platforms = r.json().get("platforms", [])
am_supported = "agentmail" in platforms
email_supported = "email" in platforms
print(f" Supported platforms: {platforms}")
print(f" email supported: {'✅ Yes' if email_supported else '❌ No'}")
print(f" agentmail supported: {'✅ Yes' if am_supported else '❌ No'}")
safe_print(f" Supported platforms: {platforms}")
safe_print(f" email supported: {icon('✅', '[OK]') + ' Yes' if email_supported else icon('❌', '[X]') + ' No'}")
safe_print(f" agentmail supported: {icon('✅', '[OK]') + ' Yes' if am_supported else icon('❌', '[X]') + ' No'}")
return am_supported
return False
@@ -136,50 +138,51 @@ def test_channel_api():
app = create_app()
client = TestClient(app)
print("\n📋 Channel API Test")
print("=" * 40)
safe_print(f"\n{icon('📋', '[TEST]')} Channel API Test")
safe_print("=" * 40)
# List channels
r = client.get("/api/channels")
channels = r.json().get("channels", [])
print(f" GET /api/channels: {len(channels)} channels")
safe_print(f" GET /api/channels: {len(channels)} channels")
for ch in channels:
icon = {"email": "📧", "agentmail": "📩"}.get(ch.get("platform", ""), "💬")
print(f" {icon} {ch.get('name', '?')} ({ch.get('platform', '?')})")
ch_icon = {"email": icon("📧", "[email]"), "agentmail": icon("📩", "[mail]")}.get(ch.get("platform", ""), icon("💬", "[chat]"))
safe_print(f" {ch_icon} {ch.get('name', '?')} ({ch.get('platform', '?')})")
# Get agentmail channel status
am_ch = next((ch for ch in channels if ch.get("platform") == "agentmail"), None)
if am_ch:
ch_id = am_ch.get("id")
r = client.get(f"/api/channels/{ch_id}/status")
print(f" GET /api/channels/{ch_id}/status: {r.json()}")
safe_print(f" GET /api/channels/{ch_id}/status: {r.json()}")
print()
safe_print()
# ── Main ────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("📩 PraisonAIUI — AgentMail Channel Integration")
print("=" * 50)
print()
safe_print(f"{icon('📩', '[MAIL]')} PraisonAIUI - AgentMail Channel Integration")
safe_print("=" * 50)
safe_print()
if "--test" in sys.argv:
# Test mode: verify APIs + seed data
print("🔍 Verifying agentmail platform support...")
safe_print(f"{icon('🔍', '[VERIFY]')} Verifying agentmail platform support...")
verify_agentmail_support()
print()
print("🌱 Seeding agentmail channel...")
safe_print()
safe_print(f"{icon('🌱', '[SEED]')} Seeding agentmail channel...")
seed_agentmail_channel()
test_channel_api()
else:
# Run the dashboard
print("🌱 Seeding channels...")
safe_print(f"{icon('🌱', '[SEED]')} Seeding channels...")
seed_agentmail_channel()
print()
safe_print()
app = create_app()
print(f"✅ Dashboard at http://localhost:8084")
print(f" Channels: http://localhost:8084/api/channels")
print(f" Platforms: http://localhost:8084/api/channels/platforms")
uvicorn.run(app, host="0.0.0.0", port=8084, log_level="info")
safe_print(f"{icon('✅', '[OK]')} Dashboard at http://localhost:8084")
safe_print(" Channels: http://localhost:8084/api/channels")
safe_print(" Platforms: http://localhost:8084/api/channels/platforms")
host = os.getenv("HOST", "127.0.0.1")
uvicorn.run(app, host=host, port=8084, log_level="info")
@@ -227,5 +227,7 @@ async def on_welcome():
app = create_app()
if __name__ == "__main__":
import os
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8083)
host = os.getenv("HOST", "127.0.0.1")
uvicorn.run(app, host=host, port=8083)
@@ -19,6 +19,22 @@ import os
import praisonaiui as aiui
try:
from openai import AsyncOpenAI
except ImportError:
AsyncOpenAI = None
# Reuse client to prevent resource churn
_client = None
def _get_client():
global _client
if _client is None and AsyncOpenAI is not None:
api_key = os.getenv("OPENAI_API_KEY")
if api_key:
_client = AsyncOpenAI(api_key=api_key)
return _client
# ── Dashboard style, but no sidebar navigation ─────────────
aiui.set_style("dashboard")
aiui.set_dashboard(sidebar=False, page_header=False)
@@ -49,18 +65,14 @@ async def on_message(message: str):
"""Stream a response from OpenAI."""
await aiui.think("Thinking...")
try:
from openai import AsyncOpenAI
except ImportError:
await aiui.say("❌ Please install openai: `pip install openai`")
client = _get_client()
if client is None:
if AsyncOpenAI is None:
await aiui.say("❌ Please install openai: `pip install openai`")
else:
await aiui.say("❌ Please set OPENAI_API_KEY environment variable.")
return
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
await aiui.say("❌ Please set OPENAI_API_KEY environment variable.")
return
client = AsyncOpenAI(api_key=api_key)
stream = await client.chat.completions.create(
model=os.getenv("PRAISONAI_MODEL", "gpt-4o-mini"),
messages=[
@@ -18,6 +18,22 @@ import os
import praisonaiui as aiui
try:
from openai import AsyncOpenAI
except ImportError:
AsyncOpenAI = None
# Reuse client to prevent resource churn
_client = None
def _get_client():
global _client
if _client is None and AsyncOpenAI is not None:
api_key = os.getenv("OPENAI_API_KEY")
if api_key:
_client = AsyncOpenAI(api_key=api_key)
return _client
# ── Register custom teal/cyan theme ──────────────────────────────────
aiui.register_theme("teal-dark", {
"accent": "#14b8a6", # teal-500
@@ -58,18 +74,14 @@ async def on_message(message: str):
"""Stream a response from OpenAI."""
await aiui.think("Thinking...")
try:
from openai import AsyncOpenAI
except ImportError:
await aiui.say("❌ Please install openai: `pip install openai`")
client = _get_client()
if client is None:
if AsyncOpenAI is None:
await aiui.say("❌ Please install openai: `pip install openai`")
else:
await aiui.say("❌ Please set OPENAI_API_KEY environment variable.")
return
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
await aiui.say("❌ Please set OPENAI_API_KEY environment variable.")
return
client = AsyncOpenAI(api_key=api_key)
stream = await client.chat.completions.create(
model=os.getenv("PRAISONAI_MODEL", "gpt-4o-mini"),
messages=[
@@ -36,17 +36,14 @@ python app.py
## Testing the custom component + custom view
The `timeline` component and the `custom-view` page are client-side extensions.
Load `plugin.js` into the page:
`app.py` loads `plugin.js` automatically via `aiui.set_custom_js()` — no DevTools paste required.
1. Open http://localhost:8082 in the browser
2. Open DevTools → Console
3. Paste the full contents of `plugin.js` and press Enter
4. Navigate to **Custom Component** → the timeline now renders as a styled list
5. Navigate to **Client-Only View** → a JS-rendered clock appears
2. Navigate to **Custom Component** → the timeline renders as a styled list
3. Navigate to **Client-Only View** → a JS-rendered clock appears
> **Gap**: There is currently no Python API to inject client-side JS from
> `app.py`. A proposed `aiui.set_custom_js(path)` would close this gap —
> see the `docs/features/how-to-add-a-feature.md` guide.
> **Note:** `aiui.set_custom_js(path)` injects client-side JS from a file path at page load.
> See `docs/features/how-to-add-a-feature.md` for the extension API contract.
## Testing the form action
@@ -214,7 +214,9 @@ async def custom_view_fallback():
if __name__ == "__main__":
import os
import uvicorn
from praisonaiui.server import create_app
app = create_app()
uvicorn.run(app, host="0.0.0.0", port=8082)
host = os.getenv("HOST", "127.0.0.1")
uvicorn.run(app, host=host, port=8082)
@@ -27,14 +27,25 @@
'display:flex;gap:16px;align-items:center;padding:12px;' +
'background:var(--db-card-bg, #1f2937);border-radius:8px;' +
'border-left:4px solid var(--db-accent, #818cf8)';
row.innerHTML =
'<div style="font-size:24px">' + (ev.icon || '•') + '</div>' +
'<div style="flex:1">' +
'<div style="font-weight:600;color:var(--db-text, #fff)">' +
(ev.label || '') + '</div>' +
'<div style="font-size:12px;color:var(--db-text-muted, #94a3b8);' +
'margin-top:4px">' + (ev.time || '') + '</div>' +
'</div>';
const iconDiv = document.createElement('div');
iconDiv.style.cssText = 'font-size:24px';
iconDiv.textContent = ev.icon || '•';
const contentDiv = document.createElement('div');
contentDiv.style.cssText = 'flex:1';
const labelDiv = document.createElement('div');
labelDiv.style.cssText = 'font-weight:600;color:var(--db-text, #fff)';
labelDiv.textContent = ev.label || '';
const timeDiv = document.createElement('div');
timeDiv.style.cssText = 'font-size:12px;color:var(--db-text-muted, #94a3b8);margin-top:4px';
timeDiv.textContent = ev.time || '';
contentDiv.appendChild(labelDiv);
contentDiv.appendChild(timeDiv);
row.appendChild(iconDiv);
row.appendChild(contentDiv);
el.appendChild(row);
});
return el;
@@ -47,12 +58,38 @@
const box = document.createElement('div');
box.style.cssText = 'padding:24px;background:var(--db-card-bg, #1f2937);' +
'border-radius:12px;color:var(--db-text, #fff)';
box.innerHTML =
'<h2 style="margin:0 0 16px 0">⚡ Client-Only View</h2>' +
'<p>This entire page is rendered by JavaScript in the browser — ' +
'it does <b>not</b> call <code>/api/pages/custom-view/data</code>.</p>' +
'<p>Registered via <code>window.aiui.registerView(\'custom-view\', ...)</code>.</p>' +
'<p>Current time: <span id="aiui-clock"></span></p>';
const h2 = document.createElement('h2');
h2.style.cssText = 'margin:0 0 16px 0';
h2.textContent = '⚡ Client-Only View';
const p1 = document.createElement('p');
p1.textContent = 'This entire page is rendered by JavaScript in the browser — it does ';
const strongCode = document.createElement('strong');
strongCode.textContent = 'not';
p1.appendChild(strongCode);
p1.appendChild(document.createTextNode(' call '));
const code1 = document.createElement('code');
code1.textContent = '/api/pages/custom-view/data';
p1.appendChild(code1);
p1.appendChild(document.createTextNode('.'));
const p2 = document.createElement('p');
p2.textContent = 'Registered via ';
const code2 = document.createElement('code');
code2.textContent = 'window.aiui.registerView(\'custom-view\', ...)';
p2.appendChild(code2);
p2.appendChild(document.createTextNode('.'));
const p3 = document.createElement('p');
p3.textContent = 'Current time: ';
const clockSpan = document.createElement('span');
clockSpan.id = 'aiui-clock';
p3.appendChild(clockSpan);
box.appendChild(h2);
box.appendChild(p1);
box.appendChild(p2);
box.appendChild(p3);
container.appendChild(box);
const clock = box.querySelector('#aiui-clock');
const tick = function () { clock.textContent = new Date().toLocaleTimeString(); };
+88
View File
@@ -0,0 +1,88 @@
"""Example 30 — A2UI canvas and send_a2ui_messages integration.
Run:
python app.py
# Optional full Google A2UI renderer in React shell:
cd ../../../frontend && npm install @a2ui/react @a2ui/web_core && npm run build
"""
from __future__ import annotations
import praisonaiui as aiui
aiui.set_style("dashboard")
aiui.set_branding(title="A2UI Canvas Demo", logo="🖼️")
aiui.set_chat_preview(enabled=True, surface_id="main", width="38%")
aiui.set_pages(["chat-canvas", "chat", "canvas", "agent-canvas"])
@aiui.page("agent-canvas", title="Agent Canvas", icon="🖼️", group="A2UI")
async def agent_canvas_page():
return {"_surface": {"id": "main", "messages": []}}
@aiui.surface_action("main")
async def on_surface_action(action: dict):
return {"received": action}
def _build_assistant_agent():
from praisonaiagents import Agent
from praisonaiagents.tools.decorator import tool
from praisonaiagents.tools.a2ui_tools import send_a2ui_messages as sdk_send_a2ui
from praisonaiui.a2ui_utils import coerce_a2ui_tool_messages
@tool
def send_a2ui_messages(messages):
"""
Push A2UI v0.9 UI to the canvas. Always call this for buttons, layouts, forms.
Example:
send_a2ui_messages(messages=[
{"createSurface": {"surfaceId": "main", "catalogId": "basic"}},
{"updateComponents": {"components": [
{"component": "Text", "text": {"literal": "Mobile App Header"}},
{"component": "Button", "text": {"literal": "Submit"},
"action": {"name": "handleSubmit"}},
]}},
])
"""
coerced = coerce_a2ui_tool_messages(messages, surface_id="main")
return sdk_send_a2ui(messages=coerced)
role = (
"You are a UI builder. For ANY UI request you MUST call send_a2ui_messages "
"before replying — never claim UI was created without a successful tool call. "
"Use surface id 'main'. Prefer Column layouts for mobile app UIs."
)
try:
from praisonaiagents.ui.a2ui.adapter import generate_a2ui_system_prompt
instructions = generate_a2ui_system_prompt(
role_description=role,
workflow_description="Call send_a2ui_messages with valid A2UI JSON, then briefly confirm.",
ui_description="Dashboard canvas on surface main.",
)
except ImportError:
instructions = role
return Agent(name="assistant", instructions=instructions, tools=[send_a2ui_messages])
def create_app():
try:
agent = _build_assistant_agent()
aiui.register_agent("assistant", agent)
except ImportError:
pass
from praisonaiui.server import create_app as _create_app
return _create_app()
if __name__ == "__main__":
import uvicorn
uvicorn.run(create_app(), host="127.0.0.1", port=8099)
+51
View File
@@ -0,0 +1,51 @@
"""Image preview in chat — ImageAgent example.
Uses praisonaiagents ``ImageAgent`` with ``PraisonAIProvider`` so generated
images appear inline in chat via ``MESSAGE_ELEMENT`` events.
Requires:
pip install praisonaiagents litellm
export OPENAI_API_KEY=your-key
Optional:
export IMAGE_MODEL=gpt-image-2 # default; override if your project uses another image model
Run:
aiui run app.py
"""
import os
import praisonaiui as aiui
from praisonaiui.providers import PraisonAIProvider
try:
from praisonaiagents import ImageAgent
except ImportError as exc:
raise ImportError("Install praisonaiagents: pip install praisonaiagents") from exc
# Match other chat examples (25-clean-chat, 26-custom-theme-chat): dashboard shell
aiui.set_style("dashboard")
aiui.set_dashboard(sidebar=False, page_header=False)
aiui.set_branding(title="Image Studio", logo="🖼️")
aiui.set_theme(preset="blue", dark_mode=True, radius="lg")
aiui.set_pages(["chat"])
image_agent = ImageAgent(
name="Image Bot",
llm=os.environ.get("IMAGE_MODEL", "gpt-image-2"),
verbose=False,
)
# Legacy DALL-E params break newer OpenAI image endpoints
image_agent.image_config.response_format = None
image_agent.image_config.style = None
aiui.set_provider(PraisonAIProvider(agent=image_agent))
@aiui.starters
async def get_starters():
return [
{"label": "Sunset over mountains", "message": "A sunset over mountains", "icon": "🌄"},
{"label": "Robot portrait", "message": "A friendly robot portrait", "icon": "🤖"},
]
Binary file not shown.
+40
View File
@@ -0,0 +1,40 @@
"""Cross-platform console output for examples.
Legacy Windows consoles default stdout to a non-UTF code page (cp1252/cp437)
that cannot encode emoji such as U+1F680. Calling ``print`` with those
characters raises ``UnicodeEncodeError`` and crashes the example before the
server starts. These helpers mirror the ``_supports_unicode``/``_icon`` pattern
in ``praisonaiui.cli`` so examples degrade to ASCII instead of crashing while
preserving emoji on UTF-aware terminals.
Usage:
from _shared.console import safe_print, icon
safe_print(f"{icon('🚀', '[START]')} Starting gateway...")
"""
from __future__ import annotations
import sys
from typing import Any
def supports_unicode() -> bool:
try:
encoding = sys.stdout.encoding or ""
except Exception:
return False
return "utf" in encoding.lower()
def icon(symbol: str, fallback: str) -> str:
return symbol if supports_unicode() else fallback
def safe_print(*args: Any, **kwargs: Any) -> None:
sep = kwargs.pop("sep", " ")
text = sep.join(str(a) for a in args)
try:
print(text, **kwargs)
except UnicodeEncodeError:
print(text.encode("ascii", errors="replace").decode("ascii"), **kwargs)
+21 -12
View File
@@ -11,33 +11,42 @@ than direct store manipulation.
def seed_demo_data():
"""Populate feature stores with demo data.
This is a no-op placeholder. Use API calls in tests to seed data,
as feature stores may use different backends (file, DB, etc.).
Seeds data via HTTP API calls to ensure compatibility across
different backend implementations (file, DB, etc.).
"""
pass
try:
from starlette.testclient import TestClient
from praisonaiui.server import create_app
app = create_app()
with TestClient(app) as client:
seed_via_api(client)
except ImportError:
pass
def seed_via_api(client):
"""Seed demo data via API calls (recommended approach).
Args:
client: Starlette TestClient instance
"""
# Channels
client.post("/api/channels", json={
"name": "Discord #general", "platform": "discord",
"config": {"guild_id": "123456789"}
"config": {"guild_id": "123456789", "bot_token_ref": "env:DISCORD_BOT_TOKEN"}
})
client.post("/api/channels", json={
"name": "Telegram Support", "platform": "telegram",
"config": {"bot_token": "***masked***"}
"config": {"bot_token_ref": "env:TELEGRAM_BOT_TOKEN"}
})
client.post("/api/channels", json={
"name": "Support Email", "platform": "email",
"config": {"email_address": "[email protected]"}
"config": {"email_address": "[email protected]", "app_password_ref": "env:EMAIL_APP_PASSWORD"}
})
# Agents
client.post("/api/agents/definitions", json={
"name": "Summarizer",
@@ -49,21 +58,21 @@ def seed_via_api(client):
"instructions": "You are an expert programmer.",
"model": "gpt-4o"
})
# Schedules
client.post("/api/schedules", json={
"name": "Daily Report",
"schedule": {"kind": "every", "every_seconds": 86400},
"message": "Generate daily summary"
})
# Usage
client.post("/api/usage/track", json={
"model": "gpt-4o-mini",
"input_tokens": 150,
"output_tokens": 80
})
# Jobs
client.post("/api/jobs", json={
"prompt": "Test job",
+121
View File
@@ -0,0 +1,121 @@
"""StreamBridge — Thread-safe async-sync bridge for agent streaming.
Solves the concurrency architecture issue where PraisonAI agent execution
runs on worker threads while PraisonAIUI chat runs on asyncio. Token streaming
crosses this boundary via callbacks that must be thread-safe.
Usage:
bridge = StreamBridge()
# In agent callback (worker thread):
bridge.emit_token(token)
# In asyncio handler:
async for token in bridge.consume():
await aiui.stream_token(token)
"""
import asyncio
import threading
from typing import Any, AsyncIterator, Callable
class StreamBridge:
"""Thread-safe bridge for streaming tokens from worker threads to asyncio."""
def __init__(self, loop: asyncio.AbstractEventLoop | None = None):
"""Initialize the bridge.
Args:
loop: Event loop to use. If None, uses the current running loop.
"""
if loop is None:
try:
loop = asyncio.get_running_loop()
except RuntimeError:
raise RuntimeError("No running event loop found. StreamBridge must be initialized from an async context (or pass an explicit loop).")
self._loop = loop
self._queue = asyncio.Queue()
self._cancelled = False
self._lock = threading.Lock()
def emit_token(self, token: str) -> None:
"""Emit a token from a worker thread (thread-safe).
Args:
token: Token to emit, or None to signal end of stream.
"""
with self._lock:
if self._cancelled:
return
# Use call_soon_threadsafe for thread-safe queue operations
self._loop.call_soon_threadsafe(self._queue.put_nowait, token)
def emit_end(self) -> None:
"""Signal end of stream (thread-safe)."""
self.emit_token(None)
async def consume(self, timeout: float = 120.0) -> AsyncIterator[str]:
"""Consume tokens asynchronously.
Args:
timeout: Timeout in seconds for each token.
Yields:
Tokens as they arrive. Stops when None is received (end signal).
"""
while not self._cancelled:
try:
token = await asyncio.wait_for(self._queue.get(), timeout=timeout)
except asyncio.TimeoutError:
break
if token is None: # End of stream sentinel
break
yield token
def cancel(self) -> None:
"""Cancel the bridge and stop token emission (thread-safe)."""
with self._lock:
self._cancelled = True
# Signal any waiting consumers
self._loop.call_soon_threadsafe(self._queue.put_nowait, None)
def emitter_callback(self) -> Callable[[Any], None]:
"""Create a callback function for agent stream emitters.
Returns:
Callback function that can be passed to agent.stream_emitter.add_callback()
"""
def callback(event):
"""Handle stream events from PraisonAI agent."""
try:
from praisonaiagents.streaming.events import StreamEventType
if event.type == StreamEventType.DELTA_TEXT and event.content:
self.emit_token(event.content)
elif event.type == StreamEventType.FIRST_TOKEN and event.content:
self.emit_token(event.content)
elif event.type == StreamEventType.STREAM_END:
self.emit_end()
except (ImportError, AttributeError):
# Fallback for other event types or missing dependencies
if hasattr(event, 'content') and event.content:
self.emit_token(event.content)
return callback
def create_stream_bridge(loop: asyncio.AbstractEventLoop | None = None) -> StreamBridge:
"""Create a new StreamBridge instance.
Args:
loop: Event loop to use. If None, uses the current running loop.
Returns:
New StreamBridge instance.
"""
return StreamBridge(loop)
@@ -0,0 +1,37 @@
"""Minimal external agent hosted on PraisonAIUI dashboard shell."""
import praisonaiui as aiui
from praisonaiui.server import create_app
aiui.set_style("dashboard")
aiui.set_branding(title="Acme Agents", logo="A")
aiui.set_pages(["chat", "tasks", "sessions", "config"])
aiui.set_dashboard(modules=["jobs"], sidebar=True, page_header=True)
@aiui.page("tasks", title="Tasks", icon="📋", group="Work", order=5)
async def tasks_page():
return aiui.layout(
[
aiui.board(
columns=[
{
"id": "todo",
"title": "Todo",
"cards": [
aiui.card("Review PR", footer="agent-a"),
aiui.card("Write docs", footer="agent-b"),
],
},
{
"id": "done",
"title": "Done",
"cards": [aiui.card("Modular shell", footer="shipped")],
},
]
),
]
)
app = create_app()
@@ -0,0 +1,34 @@
# Meeting Agent example — copy to .env and fill in. Never commit real secrets.
# Used by the PraisonAI agent for summarization/embeddings.
OPENAI_API_KEY=
# Optional: enable Slack summary delivery via the built-in SlackChannelAdapter.
SLACK_BOT_TOKEN=
SLACK_APP_TOKEN=
# Local dev server port for `python app.py`.
MEETING_AGENT_PORT=8000
# ── Recall.ai (EU workspace: Praison) ───────────────────────────────────────
# Region must match dashboard: eu-central-1 (Frankfurt).
RECALL_REGION=eu-central-1
# Optional: your Recall workspace ID (shown in the config-status API only; the
# API key already scopes requests to a workspace).
RECALL_WORKSPACE_ID=
# REST API key from Recall dashboard (Developers → API Keys). Server-side only.
RECALL_API_KEY=
# Workspace verification secret (whsec_…) from Recall MCP or dashboard.
RECALL_WEBHOOK_VERIFICATION_SECRET=
# Stable public HTTPS backend URL (reserved ngrok domain in dev). Required for
# webhooks and Google Calendar V2 OAuth callback forwarding.
PUBLIC_API_BASE_URL=
# Bot display name in meetings.
RECALL_BOT_NAME=Praison Meeting Agent
# Calendar V2: when true (default), eligible future events with video links get bots.
CALENDAR_AUTO_RECORD=true
Loaded 100 of 546 files, more files were not shown because too many files have changed in this diff. Show more