Compare commits

...
6 Commits
Author SHA1 Message Date
Mervin Praison 6d3ba7cdf8 Release v2.3.52 2025-12-19 15:34:19 +00:00
Mervin Praison 96616498d2 Release v2.3.51 2025-12-19 15:25:30 +00:00
Mervin Praison 1cad345415 Release v2.3.50 2025-12-19 15:19:29 +00:00
Mervin Praison 802e7e52f9 fix(readme): correct CLI docs URLs to include /docs/ prefix
Changed all https://docs.praison.ai/cli/* URLs to https://docs.praison.ai/docs/cli/*
Fixed 39 URLs total
2025-12-19 13:01:53 +00:00
Mervin Praison 53704f234c Release v2.3.49 2025-12-19 12:49:12 +00:00
Mervin Praison b575994d91 docs(readme): update AI commit CLI with security scanning and auto mode
- Add full auto mode (-a) with stage, security check, commit, push
- Document security scanning features
- Add --no-verify flag documentation
- List detected sensitive patterns
2025-12-19 12:35:49 +00:00
13 changed files with 2380 additions and 144 deletions

No files matched your search

+85 -41
View File
@@ -252,20 +252,20 @@ npm install praisonai
| 🪝 Hooks | [Example](#9-hooks) | [📖](https://docs.praison.ai/features/hooks) |
| 📈 Telemetry | [Example](examples/python/telemetry/production-telemetry-example.py) | [📖](https://docs.praison.ai/features/telemetry) |
| 📹 Camera Integration | [Example](examples/python/camera/) | [📖](https://docs.praison.ai/features/camera-integration) |
| 📄 Project Docs (.praison/docs/) | [Example](#docs-cli) | [📖](https://docs.praison.ai/cli/docs) |
| 🔌 MCP Config Management | [Example](#mcp-config-cli) | [📖](https://docs.praison.ai/cli/mcp) |
| 💬 AI Commit Messages | [Example](#ai-commit-cli) | [📖](https://docs.praison.ai/cli/commit) |
| @ @Mentions in Prompts | [Example](#mentions-in-prompts) | [📖](https://docs.praison.ai/cli/mentions) |
| 💾 Auto-Save Sessions | [Example](#session-management-python) | [📖](https://docs.praison.ai/cli/session) |
| 📜 History in Context | [Example](#session-management-python) | [📖](https://docs.praison.ai/cli/session) |
| 📄 Project Docs (.praison/docs/) | [Example](#docs-cli) | [📖](https://docs.praison.ai/docs/cli/docs) |
| 🔌 MCP Config Management | [Example](#mcp-config-cli) | [📖](https://docs.praison.ai/docs/cli/mcp) |
| 💬 AI Commit Messages | [Example](#ai-commit-cli) | [📖](https://docs.praison.ai/docs/cli/commit) |
| @ @Mentions in Prompts | [Example](#mentions-in-prompts) | [📖](https://docs.praison.ai/docs/cli/mentions) |
| 💾 Auto-Save Sessions | [Example](#session-management-python) | [📖](https://docs.praison.ai/docs/cli/session) |
| 📜 History in Context | [Example](#session-management-python) | [📖](https://docs.praison.ai/docs/cli/session) |
| **🖥️ CLI Features** | | |
| ↳ Slash Commands | [Example](examples/python/cli/slash_commands_example.py) | [📖](https://docs.praison.ai/cli/slash-commands) |
| ↳ Autonomy Modes | [Example](examples/python/cli/autonomy_modes_example.py) | [📖](https://docs.praison.ai/cli/autonomy-modes) |
| ↳ Cost Tracking | [Example](examples/python/cli/cost_tracking_example.py) | [📖](https://docs.praison.ai/cli/cost-tracking) |
| ↳ Repository Map | [Example](examples/python/cli/repo_map_example.py) | [📖](https://docs.praison.ai/cli/repo-map) |
| ↳ Interactive TUI | [Example](examples/python/cli/interactive_tui_example.py) | [📖](https://docs.praison.ai/cli/interactive-tui) |
| ↳ Git Integration | [Example](examples/python/cli/git_integration_example.py) | [📖](https://docs.praison.ai/cli/git-integration) |
| ↳ Sandbox Execution | [Example](examples/python/cli/sandbox_execution_example.py) | [📖](https://docs.praison.ai/cli/sandbox-execution) |
| ↳ Slash Commands | [Example](examples/python/cli/slash_commands_example.py) | [📖](https://docs.praison.ai/docs/cli/slash-commands) |
| ↳ Autonomy Modes | [Example](examples/python/cli/autonomy_modes_example.py) | [📖](https://docs.praison.ai/docs/cli/autonomy-modes) |
| ↳ Cost Tracking | [Example](examples/python/cli/cost_tracking_example.py) | [📖](https://docs.praison.ai/docs/cli/cost-tracking) |
| ↳ Repository Map | [Example](examples/python/cli/repo_map_example.py) | [📖](https://docs.praison.ai/docs/cli/repo-map) |
| ↳ Interactive TUI | [Example](examples/python/cli/interactive_tui_example.py) | [📖](https://docs.praison.ai/docs/cli/interactive-tui) |
| ↳ Git Integration | [Example](examples/python/cli/git_integration_example.py) | [📖](https://docs.praison.ai/docs/cli/git-integration) |
| ↳ Sandbox Execution | [Example](examples/python/cli/sandbox_execution_example.py) | [📖](https://docs.praison.ai/docs/cli/sandbox-execution) |
---
@@ -972,6 +972,32 @@ export OPENAI_API_KEY=xxxxxxxxxxxxxxxxxxxxxx
praisonai --auto create a movie script about Robots in Mars
```
### Interactive Mode CLI:
```bash
# Start interactive terminal mode (inspired by Gemini CLI, Codex CLI, Claude Code)
praisonai --interactive
praisonai -i
# Features:
# - Streaming responses (no boxes)
# - Built-in tools: read_file, write_file, list_files, execute_command, internet_search
# - Slash commands: /help, /exit, /tools, /clear
# Chat mode - single prompt with interactive style (for testing/scripting)
# Use --chat (or --chat-mode for backward compatibility)
praisonai "list files in current folder" --chat
praisonai "search the web for AI news" --chat
praisonai "read README.md" --chat
```
### Chat UI (Web Interface):
```bash
# Start web-based Chainlit chat interface (requires praisonai[chat])
pip install "praisonai[chat]"
praisonai chat
# Opens browser at http://localhost:8084
```
### Query Rewriting (works with any command):
```bash
# Rewrite query for better results (uses QueryRewriterAgent)
@@ -1446,13 +1472,31 @@ praisonai mcp help
### AI Commit CLI:
```bash
# Generate AI commit message for staged changes
# Full auto mode: stage all, security check, commit, and push
praisonai commit -a
# Interactive mode (requires git add first)
praisonai commit
# Generate, commit, and push
# Interactive with auto-push
praisonai commit --push
# Skip security check (not recommended)
praisonai commit -a --no-verify
```
**Features:**
- 🤖 AI-generated conventional commit messages
- 🔒 Built-in security scanning (API keys, passwords, secrets, sensitive files)
- 📦 Auto-staging with `-a` flag
- 🚀 Auto-push in full auto mode
- ✏️ Edit message before commit in interactive mode
**Security Detection:**
- API keys, secrets, tokens (AWS, GitHub, GitLab, Slack)
- Passwords and private keys
- Sensitive files (`.env`, `id_rsa`, `.pem`, `.key`, etc.)
### Serve CLI (API Server):
```bash
# Start API server for agents defined in YAML
@@ -1757,32 +1801,32 @@ app.include_router(a2a.get_router())
| Feature | Docs |
|---------|:----:|
| 🔄 Query Rewrite - RAG optimization | [📖](https://docs.praison.ai/cli/query-rewrite) |
| 🔬 Deep Research - Automated research | [📖](https://docs.praison.ai/cli/deep-research) |
| 📋 Planning - Step-by-step execution | [📖](https://docs.praison.ai/cli/planning) |
| 💾 Memory - Persistent agent memory | [📖](https://docs.praison.ai/cli/memory) |
| 📜 Rules - Auto-discovered instructions | [📖](https://docs.praison.ai/cli/rules) |
| 🔄 Workflow - Multi-step workflows | [📖](https://docs.praison.ai/cli/workflow) |
| 🪝 Hooks - Event-driven actions | [📖](https://docs.praison.ai/cli/hooks) |
| 🧠 Claude Memory - Anthropic memory tool | [📖](https://docs.praison.ai/cli/claude-memory) |
| 🛡️ Guardrail - Output validation | [📖](https://docs.praison.ai/cli/guardrail) |
| 📊 Metrics - Token usage tracking | [📖](https://docs.praison.ai/cli/metrics) |
| 🖼️ Image - Vision processing | [📖](https://docs.praison.ai/cli/image) |
| 📡 Telemetry - Usage monitoring | [📖](https://docs.praison.ai/cli/telemetry) |
| 🔌 MCP - Model Context Protocol | [📖](https://docs.praison.ai/cli/mcp) |
| ⚡ Fast Context - Codebase search | [📖](https://docs.praison.ai/cli/fast-context) |
| 📚 Knowledge - RAG management | [📖](https://docs.praison.ai/cli/knowledge) |
| 💬 Session - Conversation management | [📖](https://docs.praison.ai/cli/session) |
| 🔧 Tools - Tool discovery | [📖](https://docs.praison.ai/cli/tools) |
| 🤝 Handoff - Agent delegation | [📖](https://docs.praison.ai/cli/handoff) |
| 🧠 Auto Memory - Memory extraction | [📖](https://docs.praison.ai/cli/auto-memory) |
| 📋 Todo - Task management | [📖](https://docs.praison.ai/cli/todo) |
| 🎯 Router - Smart model selection | [📖](https://docs.praison.ai/cli/router) |
| 📈 Flow Display - Visual workflow | [📖](https://docs.praison.ai/cli/flow-display) |
| ✨ Prompt Expansion - Detailed prompts | [📖](https://docs.praison.ai/cli/prompt-expansion) |
| 🌐 Web Search - Real-time search | [📖](https://docs.praison.ai/cli/web-search) |
| 📥 Web Fetch - URL content retrieval | [📖](https://docs.praison.ai/cli/web-fetch) |
| 💾 Prompt Caching - Cost reduction | [📖](https://docs.praison.ai/cli/prompt-caching) |
| 🔄 Query Rewrite - RAG optimization | [📖](https://docs.praison.ai/docs/cli/query-rewrite) |
| 🔬 Deep Research - Automated research | [📖](https://docs.praison.ai/docs/cli/deep-research) |
| 📋 Planning - Step-by-step execution | [📖](https://docs.praison.ai/docs/cli/planning) |
| 💾 Memory - Persistent agent memory | [📖](https://docs.praison.ai/docs/cli/memory) |
| 📜 Rules - Auto-discovered instructions | [📖](https://docs.praison.ai/docs/cli/rules) |
| 🔄 Workflow - Multi-step workflows | [📖](https://docs.praison.ai/docs/cli/workflow) |
| 🪝 Hooks - Event-driven actions | [📖](https://docs.praison.ai/docs/cli/hooks) |
| 🧠 Claude Memory - Anthropic memory tool | [📖](https://docs.praison.ai/docs/cli/claude-memory) |
| 🛡️ Guardrail - Output validation | [📖](https://docs.praison.ai/docs/cli/guardrail) |
| 📊 Metrics - Token usage tracking | [📖](https://docs.praison.ai/docs/cli/metrics) |
| 🖼️ Image - Vision processing | [📖](https://docs.praison.ai/docs/cli/image) |
| 📡 Telemetry - Usage monitoring | [📖](https://docs.praison.ai/docs/cli/telemetry) |
| 🔌 MCP - Model Context Protocol | [📖](https://docs.praison.ai/docs/cli/mcp) |
| ⚡ Fast Context - Codebase search | [📖](https://docs.praison.ai/docs/cli/fast-context) |
| 📚 Knowledge - RAG management | [📖](https://docs.praison.ai/docs/cli/knowledge) |
| 💬 Session - Conversation management | [📖](https://docs.praison.ai/docs/cli/session) |
| 🔧 Tools - Tool discovery | [📖](https://docs.praison.ai/docs/cli/tools) |
| 🤝 Handoff - Agent delegation | [📖](https://docs.praison.ai/docs/cli/handoff) |
| 🧠 Auto Memory - Memory extraction | [📖](https://docs.praison.ai/docs/cli/auto-memory) |
| 📋 Todo - Task management | [📖](https://docs.praison.ai/docs/cli/todo) |
| 🎯 Router - Smart model selection | [📖](https://docs.praison.ai/docs/cli/router) |
| 📈 Flow Display - Visual workflow | [📖](https://docs.praison.ai/docs/cli/flow-display) |
| ✨ Prompt Expansion - Detailed prompts | [📖](https://docs.praison.ai/docs/cli/prompt-expansion) |
| 🌐 Web Search - Real-time search | [📖](https://docs.praison.ai/docs/cli/web-search) |
| 📥 Web Fetch - URL content retrieval | [📖](https://docs.praison.ai/docs/cli/web-fetch) |
| 💾 Prompt Caching - Cost reduction | [📖](https://docs.praison.ai/docs/cli/prompt-caching) |
---
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.48" \
"praisonai>=2.3.52" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.48" \
"praisonai>=2.3.52" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.48" \
"praisonai>=2.3.52" \
"praisonai[ui]" \
"praisonai[crewai]"
+85 -41
View File
@@ -252,20 +252,20 @@ npm install praisonai
| 🪝 Hooks | [Example](#9-hooks) | [📖](https://docs.praison.ai/features/hooks) |
| 📈 Telemetry | [Example](examples/python/telemetry/production-telemetry-example.py) | [📖](https://docs.praison.ai/features/telemetry) |
| 📹 Camera Integration | [Example](examples/python/camera/) | [📖](https://docs.praison.ai/features/camera-integration) |
| 📄 Project Docs (.praison/docs/) | [Example](#docs-cli) | [📖](https://docs.praison.ai/cli/docs) |
| 🔌 MCP Config Management | [Example](#mcp-config-cli) | [📖](https://docs.praison.ai/cli/mcp) |
| 💬 AI Commit Messages | [Example](#ai-commit-cli) | [📖](https://docs.praison.ai/cli/commit) |
| @ @Mentions in Prompts | [Example](#mentions-in-prompts) | [📖](https://docs.praison.ai/cli/mentions) |
| 💾 Auto-Save Sessions | [Example](#session-management-python) | [📖](https://docs.praison.ai/cli/session) |
| 📜 History in Context | [Example](#session-management-python) | [📖](https://docs.praison.ai/cli/session) |
| 📄 Project Docs (.praison/docs/) | [Example](#docs-cli) | [📖](https://docs.praison.ai/docs/cli/docs) |
| 🔌 MCP Config Management | [Example](#mcp-config-cli) | [📖](https://docs.praison.ai/docs/cli/mcp) |
| 💬 AI Commit Messages | [Example](#ai-commit-cli) | [📖](https://docs.praison.ai/docs/cli/commit) |
| @ @Mentions in Prompts | [Example](#mentions-in-prompts) | [📖](https://docs.praison.ai/docs/cli/mentions) |
| 💾 Auto-Save Sessions | [Example](#session-management-python) | [📖](https://docs.praison.ai/docs/cli/session) |
| 📜 History in Context | [Example](#session-management-python) | [📖](https://docs.praison.ai/docs/cli/session) |
| **🖥️ CLI Features** | | |
| ↳ Slash Commands | [Example](examples/python/cli/slash_commands_example.py) | [📖](https://docs.praison.ai/cli/slash-commands) |
| ↳ Autonomy Modes | [Example](examples/python/cli/autonomy_modes_example.py) | [📖](https://docs.praison.ai/cli/autonomy-modes) |
| ↳ Cost Tracking | [Example](examples/python/cli/cost_tracking_example.py) | [📖](https://docs.praison.ai/cli/cost-tracking) |
| ↳ Repository Map | [Example](examples/python/cli/repo_map_example.py) | [📖](https://docs.praison.ai/cli/repo-map) |
| ↳ Interactive TUI | [Example](examples/python/cli/interactive_tui_example.py) | [📖](https://docs.praison.ai/cli/interactive-tui) |
| ↳ Git Integration | [Example](examples/python/cli/git_integration_example.py) | [📖](https://docs.praison.ai/cli/git-integration) |
| ↳ Sandbox Execution | [Example](examples/python/cli/sandbox_execution_example.py) | [📖](https://docs.praison.ai/cli/sandbox-execution) |
| ↳ Slash Commands | [Example](examples/python/cli/slash_commands_example.py) | [📖](https://docs.praison.ai/docs/cli/slash-commands) |
| ↳ Autonomy Modes | [Example](examples/python/cli/autonomy_modes_example.py) | [📖](https://docs.praison.ai/docs/cli/autonomy-modes) |
| ↳ Cost Tracking | [Example](examples/python/cli/cost_tracking_example.py) | [📖](https://docs.praison.ai/docs/cli/cost-tracking) |
| ↳ Repository Map | [Example](examples/python/cli/repo_map_example.py) | [📖](https://docs.praison.ai/docs/cli/repo-map) |
| ↳ Interactive TUI | [Example](examples/python/cli/interactive_tui_example.py) | [📖](https://docs.praison.ai/docs/cli/interactive-tui) |
| ↳ Git Integration | [Example](examples/python/cli/git_integration_example.py) | [📖](https://docs.praison.ai/docs/cli/git-integration) |
| ↳ Sandbox Execution | [Example](examples/python/cli/sandbox_execution_example.py) | [📖](https://docs.praison.ai/docs/cli/sandbox-execution) |
---
@@ -972,6 +972,32 @@ export OPENAI_API_KEY=xxxxxxxxxxxxxxxxxxxxxx
praisonai --auto create a movie script about Robots in Mars
```
### Interactive Mode CLI:
```bash
# Start interactive terminal mode (inspired by Gemini CLI, Codex CLI, Claude Code)
praisonai --interactive
praisonai -i
# Features:
# - Streaming responses (no boxes)
# - Built-in tools: read_file, write_file, list_files, execute_command, internet_search
# - Slash commands: /help, /exit, /tools, /clear
# Chat mode - single prompt with interactive style (for testing/scripting)
# Use --chat (or --chat-mode for backward compatibility)
praisonai "list files in current folder" --chat
praisonai "search the web for AI news" --chat
praisonai "read README.md" --chat
```
### Chat UI (Web Interface):
```bash
# Start web-based Chainlit chat interface (requires praisonai[chat])
pip install "praisonai[chat]"
praisonai chat
# Opens browser at http://localhost:8084
```
### Query Rewriting (works with any command):
```bash
# Rewrite query for better results (uses QueryRewriterAgent)
@@ -1446,13 +1472,31 @@ praisonai mcp help
### AI Commit CLI:
```bash
# Generate AI commit message for staged changes
# Full auto mode: stage all, security check, commit, and push
praisonai commit -a
# Interactive mode (requires git add first)
praisonai commit
# Generate, commit, and push
# Interactive with auto-push
praisonai commit --push
# Skip security check (not recommended)
praisonai commit -a --no-verify
```
**Features:**
- 🤖 AI-generated conventional commit messages
- 🔒 Built-in security scanning (API keys, passwords, secrets, sensitive files)
- 📦 Auto-staging with `-a` flag
- 🚀 Auto-push in full auto mode
- ✏️ Edit message before commit in interactive mode
**Security Detection:**
- API keys, secrets, tokens (AWS, GitHub, GitLab, Slack)
- Passwords and private keys
- Sensitive files (`.env`, `id_rsa`, `.pem`, `.key`, etc.)
### Serve CLI (API Server):
```bash
# Start API server for agents defined in YAML
@@ -1757,32 +1801,32 @@ app.include_router(a2a.get_router())
| Feature | Docs |
|---------|:----:|
| 🔄 Query Rewrite - RAG optimization | [📖](https://docs.praison.ai/cli/query-rewrite) |
| 🔬 Deep Research - Automated research | [📖](https://docs.praison.ai/cli/deep-research) |
| 📋 Planning - Step-by-step execution | [📖](https://docs.praison.ai/cli/planning) |
| 💾 Memory - Persistent agent memory | [📖](https://docs.praison.ai/cli/memory) |
| 📜 Rules - Auto-discovered instructions | [📖](https://docs.praison.ai/cli/rules) |
| 🔄 Workflow - Multi-step workflows | [📖](https://docs.praison.ai/cli/workflow) |
| 🪝 Hooks - Event-driven actions | [📖](https://docs.praison.ai/cli/hooks) |
| 🧠 Claude Memory - Anthropic memory tool | [📖](https://docs.praison.ai/cli/claude-memory) |
| 🛡️ Guardrail - Output validation | [📖](https://docs.praison.ai/cli/guardrail) |
| 📊 Metrics - Token usage tracking | [📖](https://docs.praison.ai/cli/metrics) |
| 🖼️ Image - Vision processing | [📖](https://docs.praison.ai/cli/image) |
| 📡 Telemetry - Usage monitoring | [📖](https://docs.praison.ai/cli/telemetry) |
| 🔌 MCP - Model Context Protocol | [📖](https://docs.praison.ai/cli/mcp) |
| ⚡ Fast Context - Codebase search | [📖](https://docs.praison.ai/cli/fast-context) |
| 📚 Knowledge - RAG management | [📖](https://docs.praison.ai/cli/knowledge) |
| 💬 Session - Conversation management | [📖](https://docs.praison.ai/cli/session) |
| 🔧 Tools - Tool discovery | [📖](https://docs.praison.ai/cli/tools) |
| 🤝 Handoff - Agent delegation | [📖](https://docs.praison.ai/cli/handoff) |
| 🧠 Auto Memory - Memory extraction | [📖](https://docs.praison.ai/cli/auto-memory) |
| 📋 Todo - Task management | [📖](https://docs.praison.ai/cli/todo) |
| 🎯 Router - Smart model selection | [📖](https://docs.praison.ai/cli/router) |
| 📈 Flow Display - Visual workflow | [📖](https://docs.praison.ai/cli/flow-display) |
| ✨ Prompt Expansion - Detailed prompts | [📖](https://docs.praison.ai/cli/prompt-expansion) |
| 🌐 Web Search - Real-time search | [📖](https://docs.praison.ai/cli/web-search) |
| 📥 Web Fetch - URL content retrieval | [📖](https://docs.praison.ai/cli/web-fetch) |
| 💾 Prompt Caching - Cost reduction | [📖](https://docs.praison.ai/cli/prompt-caching) |
| 🔄 Query Rewrite - RAG optimization | [📖](https://docs.praison.ai/docs/cli/query-rewrite) |
| 🔬 Deep Research - Automated research | [📖](https://docs.praison.ai/docs/cli/deep-research) |
| 📋 Planning - Step-by-step execution | [📖](https://docs.praison.ai/docs/cli/planning) |
| 💾 Memory - Persistent agent memory | [📖](https://docs.praison.ai/docs/cli/memory) |
| 📜 Rules - Auto-discovered instructions | [📖](https://docs.praison.ai/docs/cli/rules) |
| 🔄 Workflow - Multi-step workflows | [📖](https://docs.praison.ai/docs/cli/workflow) |
| 🪝 Hooks - Event-driven actions | [📖](https://docs.praison.ai/docs/cli/hooks) |
| 🧠 Claude Memory - Anthropic memory tool | [📖](https://docs.praison.ai/docs/cli/claude-memory) |
| 🛡️ Guardrail - Output validation | [📖](https://docs.praison.ai/docs/cli/guardrail) |
| 📊 Metrics - Token usage tracking | [📖](https://docs.praison.ai/docs/cli/metrics) |
| 🖼️ Image - Vision processing | [📖](https://docs.praison.ai/docs/cli/image) |
| 📡 Telemetry - Usage monitoring | [📖](https://docs.praison.ai/docs/cli/telemetry) |
| 🔌 MCP - Model Context Protocol | [📖](https://docs.praison.ai/docs/cli/mcp) |
| ⚡ Fast Context - Codebase search | [📖](https://docs.praison.ai/docs/cli/fast-context) |
| 📚 Knowledge - RAG management | [📖](https://docs.praison.ai/docs/cli/knowledge) |
| 💬 Session - Conversation management | [📖](https://docs.praison.ai/docs/cli/session) |
| 🔧 Tools - Tool discovery | [📖](https://docs.praison.ai/docs/cli/tools) |
| 🤝 Handoff - Agent delegation | [📖](https://docs.praison.ai/docs/cli/handoff) |
| 🧠 Auto Memory - Memory extraction | [📖](https://docs.praison.ai/docs/cli/auto-memory) |
| 📋 Todo - Task management | [📖](https://docs.praison.ai/docs/cli/todo) |
| 🎯 Router - Smart model selection | [📖](https://docs.praison.ai/docs/cli/router) |
| 📈 Flow Display - Visual workflow | [📖](https://docs.praison.ai/docs/cli/flow-display) |
| ✨ Prompt Expansion - Detailed prompts | [📖](https://docs.praison.ai/docs/cli/prompt-expansion) |
| 🌐 Web Search - Real-time search | [📖](https://docs.praison.ai/docs/cli/web-search) |
| 📥 Web Fetch - URL content retrieval | [📖](https://docs.praison.ai/docs/cli/web-fetch) |
| 💾 Prompt Caching - Cost reduction | [📖](https://docs.praison.ai/docs/cli/prompt-caching) |
---
+2 -2
View File
@@ -3,8 +3,8 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.48.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.48.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.52.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.52.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
@@ -25,6 +25,7 @@ Features:
- slash_commands: Interactive slash commands (/help, /cost, /plan, etc.)
- autonomy_mode: Autonomy levels (suggest, auto-edit, full-auto)
- cost_tracker: Real-time cost and token tracking
- message_queue: Message queuing while agent is processing
"""
# Type hints available for IDE support
@@ -56,6 +57,7 @@ __all__ = [
'InteractiveTUIHandler',
'GitIntegrationHandler',
'SandboxExecutorHandler',
'MessageQueueHandler',
]
def __getattr__(name):
@@ -133,4 +135,7 @@ def __getattr__(name):
elif name == 'SandboxExecutorHandler':
from .sandbox_executor import SandboxExecutorHandler
return SandboxExecutorHandler
elif name == 'MessageQueueHandler':
from .message_queue import MessageQueueHandler
return MessageQueueHandler
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,587 @@
"""
Message Queue for Interactive Mode.
Provides FIFO message queuing while the AI agent is processing,
inspired by Claude Code, Windsurf Cascade, Cursor, and Gemini CLI.
Features:
- Queue messages while agent is processing
- Auto-process queue when agent becomes idle
- Visual indicators for queue status
- Thread-safe operations
"""
import threading
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Callable, List, Optional
# ============================================================================
# Enums
# ============================================================================
class ProcessingState(Enum):
"""State of the agent processing."""
IDLE = "idle"
PROCESSING = "processing"
WAITING_APPROVAL = "waiting_approval"
class MessagePriority(Enum):
"""Priority levels for queued messages."""
LOW = "low"
NORMAL = "normal"
HIGH = "high"
# ============================================================================
# Data Classes
# ============================================================================
@dataclass
class QueuedMessage:
"""A message in the queue with metadata."""
content: str
timestamp: float = field(default_factory=time.time)
priority: MessagePriority = MessagePriority.NORMAL
# ============================================================================
# MessageQueue - Thread-safe FIFO queue
# ============================================================================
class MessageQueue:
"""
Thread-safe FIFO message queue.
Provides basic queue operations with thread safety for concurrent access.
"""
def __init__(self):
self._messages: List[str] = []
self._lock = threading.Lock()
@property
def is_empty(self) -> bool:
"""Check if queue is empty."""
with self._lock:
return len(self._messages) == 0
@property
def count(self) -> int:
"""Get number of messages in queue."""
with self._lock:
return len(self._messages)
def add(self, message: str) -> bool:
"""
Add a message to the end of the queue.
Args:
message: The message to add
Returns:
True if message was added, False if empty/whitespace
"""
trimmed = message.strip() if message else ""
if not trimmed:
return False
with self._lock:
self._messages.append(trimmed)
return True
def pop(self) -> Optional[str]:
"""
Remove and return the first message (FIFO).
Returns:
The first message, or None if queue is empty
"""
with self._lock:
if self._messages:
return self._messages.pop(0)
return None
def peek(self) -> Optional[str]:
"""
View the first message without removing it.
Returns:
The first message, or None if queue is empty
"""
with self._lock:
if self._messages:
return self._messages[0]
return None
def clear(self) -> None:
"""Remove all messages from the queue."""
with self._lock:
self._messages.clear()
def get_all(self) -> List[str]:
"""
Get all messages as a list (does not remove them).
Returns:
Copy of all messages in queue order
"""
with self._lock:
return self._messages.copy()
def remove_at(self, index: int) -> Optional[str]:
"""
Remove message at specific index.
Args:
index: The index to remove (0-based)
Returns:
The removed message, or None if index is invalid
"""
with self._lock:
if 0 <= index < len(self._messages):
return self._messages.pop(index)
return None
# ============================================================================
# StateManager - Processing state management
# ============================================================================
class StateManager:
"""
Manages the processing state with optional callbacks.
Tracks whether the agent is idle, processing, or waiting for approval.
"""
def __init__(self, on_state_change: Optional[Callable[[ProcessingState, ProcessingState], None]] = None):
self._state = ProcessingState.IDLE
self._lock = threading.Lock()
self._on_state_change = on_state_change
@property
def current_state(self) -> ProcessingState:
"""Get current processing state."""
with self._lock:
return self._state
@property
def is_idle(self) -> bool:
"""Check if currently idle."""
return self.current_state == ProcessingState.IDLE
@property
def is_processing(self) -> bool:
"""Check if currently processing."""
return self.current_state == ProcessingState.PROCESSING
def set_state(self, new_state: ProcessingState) -> None:
"""
Set the processing state.
Args:
new_state: The new state to set
"""
with self._lock:
old_state = self._state
self._state = new_state
# Call callback outside lock to prevent deadlock
if self._on_state_change and old_state != new_state:
self._on_state_change(old_state, new_state)
# ============================================================================
# QueueDisplay - Visual formatting
# ============================================================================
class QueueDisplay:
"""
Formats queue and status for display.
Provides visual indicators for queue status, inspired by Codex CLI.
"""
def __init__(
self,
queue: MessageQueue,
state_manager: Optional[StateManager] = None,
max_message_length: int = 50
):
self._queue = queue
self._state_manager = state_manager
self._max_length = max_message_length
def format_queue(self) -> str:
"""
Format queued messages for display.
Returns:
Formatted string with queued messages
"""
messages = self._queue.get_all()
if not messages:
return ""
lines = []
for msg in messages:
# Truncate long messages
display_msg = msg
if len(msg) > self._max_length:
display_msg = msg[:self._max_length - 3] + "..."
lines.append(f" ↳ {display_msg}")
return "\n".join(lines)
def format_status(self) -> str:
"""
Format current processing status.
Returns:
Status string (e.g., "⏳ Processing..." or "")
"""
if not self._state_manager:
return ""
state = self._state_manager.current_state
if state == ProcessingState.PROCESSING:
return "⏳ Processing..."
elif state == ProcessingState.WAITING_APPROVAL:
return "🔒 Waiting for approval..."
return ""
def format_queue_count(self) -> str:
"""
Format queue count indicator.
Returns:
Count string (e.g., "📋 Queued (2)")
"""
count = self._queue.count
if count == 0:
return ""
return f"📋 Queued ({count})"
# ============================================================================
# MessageQueueHandler - Main handler class
# ============================================================================
class MessageQueueHandler:
"""
Main handler for message queue functionality.
Coordinates queue, state, and processing.
"""
def __init__(
self,
processor: Optional[Callable[[str], str]] = None,
on_response: Optional[Callable[[str], None]] = None
):
self._processor = processor
self._on_response = on_response
self._queue = MessageQueue()
self._state_manager = StateManager(on_state_change=self._on_state_change)
self._processing_lock = threading.Lock()
@property
def queue(self) -> MessageQueue:
"""Get the message queue."""
return self._queue
@property
def state_manager(self) -> StateManager:
"""Get the state manager."""
return self._state_manager
def submit(self, message: str) -> bool:
"""
Submit a message for processing.
If idle, processes immediately. If processing, queues the message.
Args:
message: The message to submit
Returns:
True if processed/queued, False if empty message
"""
trimmed = message.strip() if message else ""
if not trimmed:
return False
if self._state_manager.is_idle:
# Process immediately
self._process_message(trimmed)
else:
# Queue for later
self._queue.add(trimmed)
return True
def on_processing_complete(self) -> None:
"""
Called when current processing completes.
Sets state to idle and processes next queued message if any.
"""
self._state_manager.set_state(ProcessingState.IDLE)
self._process_next_in_queue()
def get_status(self) -> dict:
"""
Get current status.
Returns:
Dict with queue_count and state
"""
return {
'queue_count': self._queue.count,
'state': self._state_manager.current_state.value,
'messages': self._queue.get_all()
}
def clear_queue(self) -> None:
"""Clear all queued messages."""
self._queue.clear()
def _process_message(self, message: str) -> None:
"""Process a single message."""
if not self._processor:
return
with self._processing_lock:
self._state_manager.set_state(ProcessingState.PROCESSING)
try:
response = self._processor(message)
if self._on_response and response:
self._on_response(response)
finally:
self._state_manager.set_state(ProcessingState.IDLE)
def _process_next_in_queue(self) -> None:
"""Process the next message in queue if any."""
next_msg = self._queue.pop()
if next_msg:
self._process_message(next_msg)
def _on_state_change(self, old_state: ProcessingState, new_state: ProcessingState) -> None:
"""Handle state changes."""
# Auto-process queue when becoming idle
if new_state == ProcessingState.IDLE and old_state == ProcessingState.PROCESSING:
# Don't auto-process here to avoid recursion
# The on_processing_complete method handles this
pass
# ============================================================================
# AsyncProcessor - Background processing
# ============================================================================
class AsyncProcessor:
"""
Runs work functions in background threads.
Allows agent processing to happen asynchronously while
the main thread remains responsive for user input.
"""
def __init__(
self,
on_complete: Optional[Callable[[str], None]] = None,
on_status: Optional[Callable[[str], None]] = None,
on_error: Optional[Callable[[Exception], None]] = None
):
self._on_complete = on_complete
self._on_status = on_status
self._on_error = on_error
self._thread: Optional[threading.Thread] = None
self._running = False
self._lock = threading.Lock()
@property
def is_running(self) -> bool:
"""Check if currently processing."""
with self._lock:
return self._running
def start(self, work_fn: Callable[[], str]) -> None:
"""
Start processing work in background thread.
Args:
work_fn: Function to execute (should return result string)
"""
def _worker():
try:
with self._lock:
self._running = True
result = work_fn()
with self._lock:
self._running = False
if self._on_complete:
self._on_complete(result)
except Exception as e:
with self._lock:
self._running = False
if self._on_error:
self._on_error(e)
self._thread = threading.Thread(target=_worker, daemon=True)
self._thread.start()
def update_status(self, status: str) -> None:
"""Update status (can be called from work function)."""
if self._on_status:
self._on_status(status)
# ============================================================================
# LiveStatusDisplay - Real-time status display
# ============================================================================
class LiveStatusDisplay:
"""
Tracks and displays real-time status during processing.
Shows tool calls, command executions, and current status.
"""
def __init__(self):
self._status = ""
self._tool_calls: List[dict] = []
self._commands: List[str] = []
self._lock = threading.Lock()
@property
def current_status(self) -> str:
"""Get current status message."""
with self._lock:
return self._status
@property
def tool_calls(self) -> List[dict]:
"""Get list of tool calls."""
with self._lock:
return self._tool_calls.copy()
@property
def commands(self) -> List[str]:
"""Get list of commands executed."""
with self._lock:
return self._commands.copy()
def update_status(self, status: str) -> None:
"""Update current status message."""
with self._lock:
self._status = status
def add_tool_call(self, name: str, args: dict) -> None:
"""Add a tool call to the display."""
with self._lock:
self._tool_calls.append({
'name': name,
'args': args,
'timestamp': time.time()
})
def add_command(self, command: str) -> None:
"""Add a command execution to the display."""
with self._lock:
self._commands.append(command)
def format(self) -> str:
"""Format current status for display."""
lines = []
with self._lock:
if self._status:
lines.append(f"⏳ {self._status}")
for tool in self._tool_calls[-3:]: # Show last 3 tools
lines.append(f" 🔧 {tool['name']}")
for cmd in self._commands[-2:]: # Show last 2 commands
display_cmd = cmd[:40] + "..." if len(cmd) > 40 else cmd
lines.append(f" 💻 {display_cmd}")
return "\n".join(lines)
def clear(self) -> None:
"""Clear all status."""
with self._lock:
self._status = ""
self._tool_calls.clear()
self._commands.clear()
# ============================================================================
# NonBlockingInput - Async input handling
# ============================================================================
class NonBlockingInput:
"""
Handles user input asynchronously.
Allows users to type new messages while processing is ongoing.
"""
def __init__(self):
self._queue = MessageQueue()
@property
def has_pending(self) -> bool:
"""Check if there are pending inputs."""
return not self._queue.is_empty
@property
def pending_count(self) -> int:
"""Get number of pending inputs."""
return self._queue.count
def submit(self, message: str) -> bool:
"""Submit a new input message."""
return self._queue.add(message)
def pop(self) -> Optional[str]:
"""Get next pending input."""
return self._queue.pop()
def peek(self) -> Optional[str]:
"""View next pending input without removing."""
return self._queue.peek()
def get_all(self) -> List[str]:
"""Get all pending inputs."""
return self._queue.get_all()
def clear(self) -> None:
"""Clear all pending inputs."""
self._queue.clear()
# ============================================================================
# Exports
# ============================================================================
__all__ = [
'ProcessingState',
'MessagePriority',
'QueuedMessage',
'MessageQueue',
'StateManager',
'QueueDisplay',
'MessageQueueHandler',
'AsyncProcessor',
'LiveStatusDisplay',
'NonBlockingInput',
]
+799 -55
View File
@@ -2,9 +2,20 @@
import sys
import argparse
import warnings
import os
# Suppress crewai RuntimeWarning about module loading order (only in non-debug mode)
# This warning is harmless and occurs when running as `python -m praisonai.cli.main`
if os.environ.get('LOGLEVEL', 'INFO').upper() != 'DEBUG':
warnings.filterwarnings(
"ignore",
message=".*found in sys.modules after import of package.*",
category=RuntimeWarning
)
from praisonai.version import __version__
import yaml
import os
import time
from rich import print
from dotenv import load_dotenv
@@ -214,6 +225,12 @@ class PraisonAI:
if getattr(args, 'interactive', False):
self._start_interactive_mode(args)
return None
# Handle --chat-mode flag - run single prompt in interactive style (non-interactive)
if getattr(args, 'chat_mode', False):
prompt = args.command or getattr(args, 'direct_prompt', None)
if prompt:
return self._run_chat_mode(prompt, args)
self.framework = args.framework or self.framework
@@ -698,6 +715,7 @@ class PraisonAI:
# Interactive TUI - terminal interface with slash commands
parser.add_argument("--interactive", "-i", action="store_true", help="Start interactive terminal mode with slash commands")
parser.add_argument("--chat-mode", "--chat", action="store_true", dest="chat_mode", help="Run single prompt in interactive style (non-interactive, for testing)")
# Autonomy Mode - control AI action approval
parser.add_argument("--autonomy", type=str, choices=["suggest", "auto_edit", "full_auto"], help="Set autonomy mode for AI actions")
@@ -3766,76 +3784,802 @@ Now, {final_instruction.lower()}:"""
sys.exit(1)
def _start_interactive_mode(self, args):
"""Start interactive TUI mode with slash commands."""
"""
Start interactive TUI mode with streaming responses and tool support.
UX inspired by Gemini CLI, Codex CLI, and Claude Code:
- Streaming text output (no boxes)
- Tool status indicators
- Built-in tools (file ops, shell, web search)
- Autocomplete for slash commands and @file mentions
- /compact for context compression
- /model for model switching
- /stats for token/cost tracking
- /undo for reverting changes
"""
try:
from praisonai.cli.features import InteractiveTUIHandler, SlashCommandHandler, CostTrackerHandler
from praisonai.cli.features.interactive_tui import InteractiveConfig
from rich.console import Console
import os
# Set interactive mode flag to suppress verbose agent output
console = Console()
# Set interactive mode flag
self._interactive_mode = True
# Initialize handlers
slash_handler = SlashCommandHandler()
cost_handler = CostTrackerHandler()
cost_handler.initialize()
# Load interactive tools
tools_list = self._load_interactive_tools()
# Configure interactive session
config = InteractiveConfig(
prompt="praisonai> ",
multiline=False,
enable_completions=True,
show_status_bar=True
# Import message queue components
from praisonai.cli.features.message_queue import (
MessageQueue, StateManager, QueueDisplay, ProcessingState,
AsyncProcessor, LiveStatusDisplay
)
import threading
# Define input handler
def on_input(text):
"""Handle regular input - send to agent."""
if not text.strip():
return ""
# Create message queue and state manager
message_queue = MessageQueue()
state_manager = StateManager()
queue_display = QueueDisplay(message_queue, state_manager)
live_status = LiveStatusDisplay()
processing_lock = threading.Lock()
# Session state
session_state = {
'show_profiling': False,
'current_model': getattr(args, 'llm', None) or os.environ.get('OPENAI_MODEL_NAME', 'gpt-4o-mini'),
'conversation_history': [],
'total_input_tokens': 0,
'total_output_tokens': 0,
'total_cost': 0.0,
'request_count': 0,
'undo_stack': [], # Stack of (prompt, response) for undo
'message_queue': message_queue,
'state_manager': state_manager,
'queue_display': queue_display,
'live_status': live_status,
'processing_lock': processing_lock,
'async_processor': None, # Will be created per-request
}
# Try to use prompt_toolkit for autocomplete
prompt_session = None
try:
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.history import InMemoryHistory
# Track cost if enabled
result = self.handle_direct_prompt(text)
return result
# Define command handler
def on_command(cmd):
"""Handle slash commands."""
result = slash_handler.execute(cmd)
if result.get("type") == "exit":
return result
# Create completer for slash commands
commands = ['help', 'exit', 'quit', 'clear', 'tools', 'profile', 'model', 'stats', 'compact', 'undo', 'queue', 'q']
slash_completer = WordCompleter(
[f'/{cmd}' for cmd in commands],
ignore_case=True
)
# Handle special commands
if result.get("type") == "cost":
return {"type": "info", "message": cost_handler.get_summary()}
return result
prompt_session = PromptSession(
message="❯ ",
completer=slash_completer,
auto_suggest=AutoSuggestFromHistory(),
history=InMemoryHistory()
)
except ImportError:
pass # Fall back to simple input
# Initialize TUI handler
tui_handler = InteractiveTUIHandler()
session = tui_handler.initialize(
config=config,
on_input=on_input,
on_command=on_command
)
# Add slash commands for completion
session.add_commands(["help", "exit", "quit", "cost", "tokens", "model", "diff", "commit", "undo", "plan", "map", "clear", "settings"])
print("[bold green]PraisonAI Interactive Mode[/bold green]")
print("Type your prompt or use /help for commands. /exit to quit.\n")
# Run the interactive session
tui_handler.run()
# Print welcome message
console.print("\n[bold cyan]PraisonAI Interactive Mode[/bold cyan]")
console.print("[dim]Type your prompt, use /help for commands, /exit to quit[/dim]")
console.print(f"[dim]Model: {session_state['current_model']} | Tools: {len(tools_list)}[/dim]")
console.print("[dim]Use @file.txt to include file content | Queue messages while processing[/dim]\n")
running = True
while running:
try:
# Get user input (with autocomplete if available)
if prompt_session:
user_input = prompt_session.prompt().strip()
else:
user_input = input("❯ ").strip()
if not user_input:
continue
# Handle slash commands
if user_input.startswith("/"):
cmd_parts = user_input[1:].split(maxsplit=1)
cmd = cmd_parts[0].lower() if cmd_parts else ""
cmd_args = cmd_parts[1] if len(cmd_parts) > 1 else ""
if cmd in ["exit", "quit", "q"]:
console.print("[dim]Goodbye![/dim]")
running = False
continue
elif cmd == "help":
self._print_interactive_help(console)
continue
elif cmd == "clear":
console.clear()
continue
elif cmd == "tools":
console.print(f"[cyan]Available tools: {len(tools_list)}[/cyan]")
for tool in tools_list:
name = getattr(tool, '__name__', str(tool))
console.print(f" • {name}")
continue
elif cmd == "profile":
session_state['show_profiling'] = not session_state['show_profiling']
status = "enabled" if session_state['show_profiling'] else "disabled"
console.print(f"[cyan]Profiling {status}[/cyan]")
continue
elif cmd == "model":
self._handle_model_command(console, cmd_args, session_state)
continue
elif cmd == "stats":
self._handle_stats_command(console, session_state)
continue
elif cmd == "compact":
self._handle_compact_command(console, session_state)
continue
elif cmd == "undo":
self._handle_undo_command(console, session_state)
continue
elif cmd == "queue":
self._handle_queue_command(console, cmd_args, session_state)
continue
else:
console.print(f"[yellow]Unknown command: /{cmd}. Type /help for available commands.[/yellow]")
continue
# Process @file mentions before sending to LLM
processed_input = self._process_at_mentions(user_input, console)
# Check if already processing - queue if so
if state_manager.is_processing:
message_queue.add(processed_input)
queue_count = message_queue.count
console.print(f"[dim]📋 Queued ({queue_count}) - processing will continue in order[/dim]")
continue
# Start async processing
self._process_interactive_prompt_async(
processed_input,
tools_list,
console,
session_state=session_state
)
except KeyboardInterrupt:
console.print("\n[dim]Use /exit to quit[/dim]")
except EOFError:
running = False
except ImportError as e:
print(f"[red]ERROR: Interactive mode requires additional dependencies: {e}[/red]")
print("Install with: pip install prompt_toolkit")
print(f"[red]ERROR: Interactive mode requires rich: {e}[/red]")
print("Install with: pip install rich")
sys.exit(1)
except KeyboardInterrupt:
print("\n[yellow]Exiting interactive mode.[/yellow]")
except Exception as e:
print(f"[red]ERROR: Interactive mode failed: {e}[/red]")
import traceback
traceback.print_exc()
sys.exit(1)
def _load_interactive_tools(self):
"""Load tools for interactive mode."""
tools_list = []
try:
from praisonaiagents.tools import (
read_file as tool_read_file,
write_file as tool_write_file,
list_files as tool_list_files,
execute_command,
internet_search
)
tools_list = [tool_read_file, tool_write_file, tool_list_files, execute_command, internet_search]
except ImportError:
# Try individual imports
try:
from praisonaiagents.tools import read_file as tool_read_file
tools_list.append(tool_read_file)
except ImportError:
pass
try:
from praisonaiagents.tools import write_file as tool_write_file
tools_list.append(tool_write_file)
except ImportError:
pass
try:
from praisonaiagents.tools import list_files as tool_list_files
tools_list.append(tool_list_files)
except ImportError:
pass
try:
from praisonaiagents.tools import execute_command
tools_list.append(execute_command)
except ImportError:
pass
try:
from praisonaiagents.tools import internet_search
tools_list.append(internet_search)
except ImportError:
pass
return tools_list
def _print_interactive_help(self, console):
"""Print help for interactive mode."""
console.print("\n[bold]Commands:[/bold]")
console.print(" /help - Show this help")
console.print(" /exit - Exit interactive mode")
console.print(" /clear - Clear screen")
console.print(" /tools - List available tools")
console.print(" /profile - Toggle profiling (show timing breakdown)")
console.print(" /model [name] - Show or change current model")
console.print(" /stats - Show session statistics (tokens, cost)")
console.print(" /compact - Compress conversation history")
console.print(" /undo - Undo last response")
console.print(" /queue - Show queued messages")
console.print(" /queue clear - Clear message queue")
console.print("\n[bold]@ Mentions:[/bold]")
console.print(" @file.txt - Include file content in prompt")
console.print(" @src/ - Include directory listing")
console.print("\n[bold]Features:[/bold]")
console.print(" • File operations (read, write, list)")
console.print(" • Shell command execution")
console.print(" • Web search")
console.print(" • Context compression for long sessions")
console.print(" • Queue messages while agent is processing")
console.print("")
def _process_at_mentions(self, user_input, console):
"""
Process @file mentions in user input.
Supports:
- @file.txt - Include file content
- @path/to/file - Include file content
- @directory/ - Include directory listing
Inspired by Gemini CLI and Claude Code @file syntax.
"""
import re
import os
# Pattern to match @path (but not email addresses)
# Must be at start or preceded by whitespace
pattern = r'(?:^|\s)@([^\s@]+)'
matches = re.findall(pattern, user_input)
if not matches:
return user_input
processed_input = user_input
file_contents = []
for path in matches:
# Clean up the path
clean_path = path.strip()
# Expand ~ to home directory
if clean_path.startswith('~'):
clean_path = os.path.expanduser(clean_path)
# Make absolute if relative
if not os.path.isabs(clean_path):
clean_path = os.path.abspath(clean_path)
try:
if os.path.isfile(clean_path):
# Read file content
with open(clean_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Truncate if too large (>50KB)
if len(content) > 50000:
content = content[:50000] + "\n... [truncated, file too large]"
file_contents.append(f"\n--- Content of {path} ---\n{content}\n--- End of {path} ---\n")
console.print(f"[dim]📄 Included: {path} ({len(content)} chars)[/dim]")
elif os.path.isdir(clean_path):
# List directory contents
try:
entries = os.listdir(clean_path)
# Filter out hidden files and common ignore patterns
entries = [e for e in entries if not e.startswith('.') and e not in ['node_modules', '__pycache__', 'venv', '.git']]
entries.sort()
listing = "\n".join(f" {e}" for e in entries[:50])
if len(entries) > 50:
listing += f"\n ... and {len(entries) - 50} more files"
file_contents.append(f"\n--- Directory listing of {path} ---\n{listing}\n--- End of {path} ---\n")
console.print(f"[dim]📁 Listed: {path} ({len(entries)} items)[/dim]")
except PermissionError:
console.print(f"[yellow]⚠ Permission denied: {path}[/yellow]")
else:
console.print(f"[yellow]⚠ Not found: {path}[/yellow]")
except Exception as e:
console.print(f"[yellow]⚠ Error reading {path}: {e}[/yellow]")
# Remove @mentions from the original input and append file contents
for path in matches:
processed_input = processed_input.replace(f"@{path}", "")
processed_input = processed_input.strip()
if file_contents:
processed_input = processed_input + "\n" + "\n".join(file_contents)
return processed_input
def _handle_model_command(self, console, args, session_state):
"""Handle /model command - show or change current model."""
if not args:
# Show current model
console.print(f"[cyan]Current model: {session_state['current_model']}[/cyan]")
console.print("\n[dim]Available models (examples):[/dim]")
console.print(" • gpt-4o, gpt-4o-mini")
console.print(" • claude-3-5-sonnet, claude-3-haiku")
console.print(" • gemini-2.0-flash, gemini-1.5-pro")
console.print("\n[dim]Usage: /model <model-name>[/dim]")
else:
# Change model
new_model = args.strip()
old_model = session_state['current_model']
session_state['current_model'] = new_model
console.print(f"[green]✓ Model changed: {old_model} → {new_model}[/green]")
def _handle_stats_command(self, console, session_state):
"""Handle /stats command - show session statistics."""
from praisonai.cli.features.cost_tracker import get_pricing
console.print("\n[bold cyan]Session Statistics[/bold cyan]")
console.print(f" Model: {session_state['current_model']}")
console.print(f" Requests: {session_state['request_count']}")
console.print(f" Input tokens: {session_state['total_input_tokens']:,}")
console.print(f" Output tokens: {session_state['total_output_tokens']:,}")
console.print(f" Total tokens: {session_state['total_input_tokens'] + session_state['total_output_tokens']:,}")
# Calculate estimated cost
try:
pricing = get_pricing(session_state['current_model'])
cost = pricing.calculate_cost(
session_state['total_input_tokens'],
session_state['total_output_tokens']
)
console.print(f" Estimated cost: ${cost:.4f}")
except Exception:
pass
# Show conversation history size
history_len = len(session_state['conversation_history'])
console.print(f" History turns: {history_len}")
console.print("")
def _handle_compact_command(self, console, session_state):
"""
Handle /compact command - compress conversation history.
Inspired by Claude Code's /compact and Gemini CLI's /compress.
Uses LLM to summarize older conversation turns while keeping recent ones.
"""
history = session_state['conversation_history']
if len(history) < 4:
console.print("[yellow]Not enough conversation history to compact (need at least 4 turns)[/yellow]")
return
console.print("[dim]Compacting conversation history...[/dim]")
try:
from praisonaiagents import Agent
# Keep the last 2 turns (4 messages: 2 user + 2 assistant)
keep_count = 4
to_compress = history[:-keep_count] if len(history) > keep_count else []
to_keep = history[-keep_count:] if len(history) > keep_count else history
if not to_compress:
console.print("[yellow]Not enough old history to compress[/yellow]")
return
# Format history for summarization
history_text = ""
for i, msg in enumerate(to_compress):
role = msg.get('role', 'unknown')
content = msg.get('content', '')
if len(content) > 500:
content = content[:500] + "..."
history_text += f"{role}: {content}\n\n"
# Create summarization prompt
summary_prompt = f"""Summarize the following conversation history into a concise state snapshot.
Focus on:
- Key facts and decisions made
- Important context that should be remembered
- Any ongoing tasks or goals
Conversation to summarize:
{history_text}
Provide a concise summary (max 200 words):"""
# Use a lightweight agent for summarization
summarizer = Agent(
name="Summarizer",
role="Conversation Summarizer",
goal="Create concise summaries of conversations",
backstory="You summarize conversations while preserving key information.",
verbose=False,
llm=session_state['current_model']
)
summary = summarizer.chat(summary_prompt, stream=False)
# Replace old history with summary + recent turns
new_history = [
{"role": "system", "content": f"[Previous conversation summary]: {summary}"},
*to_keep
]
old_count = len(history)
session_state['conversation_history'] = new_history
new_count = len(new_history)
console.print(f"[green]✓ Compacted {old_count} turns → {new_count} turns[/green]")
console.print(f"[dim]Summary: {str(summary)[:100]}...[/dim]")
except Exception as e:
console.print(f"[red]Error compacting: {e}[/red]")
def _handle_undo_command(self, console, session_state):
"""
Handle /undo command - undo the last response.
Inspired by Codex CLI's /undo and Gemini CLI's /restore.
Removes the last user prompt and assistant response from history.
"""
history = session_state['conversation_history']
undo_stack = session_state['undo_stack']
if len(history) < 2:
console.print("[yellow]Nothing to undo[/yellow]")
return
# Remove last assistant response and user prompt
if len(history) >= 2:
removed_assistant = history.pop()
removed_user = history.pop()
# Store in undo stack for potential redo
undo_stack.append((removed_user, removed_assistant))
console.print("[green]✓ Undone last turn[/green]")
console.print(f"[dim]Removed: {str(removed_user.get('content', ''))[:50]}...[/dim]")
else:
console.print("[yellow]Not enough history to undo[/yellow]")
def _handle_queue_command(self, console, args, session_state):
"""
Handle /queue command - show or manage message queue.
Usage:
- /queue - Show queued messages
- /queue clear - Clear the queue
- /queue remove N - Remove message at index N
"""
message_queue = session_state.get('message_queue')
state_manager = session_state.get('state_manager')
queue_display = session_state.get('queue_display')
if not message_queue:
console.print("[yellow]Message queue not initialized[/yellow]")
return
args = args.strip().lower() if args else ""
if args == "clear":
count = message_queue.count
message_queue.clear()
console.print(f"[green]✓ Cleared {count} queued message(s)[/green]")
elif args.startswith("remove "):
try:
index = int(args.split()[1])
removed = message_queue.remove_at(index)
if removed:
console.print(f"[green]✓ Removed: {removed[:50]}...[/green]")
else:
console.print(f"[yellow]Invalid index: {index}[/yellow]")
except (ValueError, IndexError):
console.print("[yellow]Usage: /queue remove <index>[/yellow]")
else:
# Show queue status
if state_manager:
status = queue_display.format_status() if queue_display else ""
if status:
console.print(f"[cyan]{status}[/cyan]")
if message_queue.is_empty:
console.print("[dim]No messages in queue[/dim]")
else:
console.print(f"\n[bold cyan]Queued Messages ({message_queue.count}):[/bold cyan]")
for i, msg in enumerate(message_queue.get_all()):
display_msg = msg[:60] + "..." if len(msg) > 60 else msg
console.print(f" {i}. ↳ {display_msg}")
console.print("\n[dim]Use /queue clear to clear, /queue remove N to remove[/dim]")
def _run_chat_mode(self, prompt, args):
"""
Run a single prompt in interactive style (non-interactive mode for testing).
Usage: praisonai "your prompt" --chat
praisonai "your prompt" --chat-mode (alias)
This runs the prompt using the same agent/tools as interactive mode
but exits after one response (useful for testing and scripting).
"""
from rich.console import Console
console = Console()
self._interactive_mode = True # Use interactive mode settings
# Load tools
tools_list = self._load_interactive_tools()
console.print(f"[dim]Chat mode: {len(tools_list)} tools available[/dim]")
console.print(f"[dim]Prompt: {prompt[:50]}{'...' if len(prompt) > 50 else ''}[/dim]\n")
# Process the prompt with profiling enabled for testing
self._process_interactive_prompt(prompt, tools_list, console, show_profiling=True)
return None
def _process_interactive_prompt_async(self, prompt, tools_list, console, session_state=None):
"""
Process a prompt asynchronously with live status display.
Shows real-time status (tools, commands) while processing in background.
Allows user to queue more messages while processing.
"""
import threading
import time
from rich.live import Live
from rich.panel import Panel
from rich.text import Text
from praisonai.cli.features.message_queue import ProcessingState
state_manager = session_state['state_manager']
message_queue = session_state['message_queue']
live_status = session_state['live_status']
# Set processing state
state_manager.set_state(ProcessingState.PROCESSING)
live_status.clear()
live_status.update_status("Thinking...")
# Result container for thread communication
result_container = {'response': None, 'error': None, 'done': False}
def process_in_background():
"""Run agent processing in background thread."""
try:
from praisonaiagents import Agent
import logging
import warnings
# Suppress noisy loggers
for logger_name in ["httpx", "httpcore", "duckduckgo_search", "crawl4ai"]:
logging.getLogger(logger_name).setLevel(logging.WARNING)
with warnings.catch_warnings():
warnings.filterwarnings("ignore")
model = session_state.get('current_model')
live_status.update_status("Creating agent...")
# Create agent with tools
agent = Agent(
name="Assistant",
role="Helpful AI Assistant",
goal="Help the user with their tasks",
backstory="You are a helpful AI assistant with access to tools.",
tools=tools_list if tools_list else None,
verbose=False,
llm=model
)
live_status.update_status("Calling LLM...")
# Get response
response = agent.chat(prompt, stream=False)
result_container['response'] = str(response) if response else ""
except Exception as e:
result_container['error'] = e
finally:
result_container['done'] = True
# Start background thread
bg_thread = threading.Thread(target=process_in_background, daemon=True)
bg_thread.start()
# Show live status while processing
try:
with Live(console=console, refresh_per_second=4, transient=True) as live:
while not result_container['done']:
# Build status display
status_text = Text()
status_text.append("⏳ ", style="cyan")
status_text.append(live_status.current_status or "Processing...", style="dim")
# Show queued messages if any
queue_count = message_queue.count
if queue_count > 0:
status_text.append(f"\n📋 Queued: {queue_count}", style="dim yellow")
live.update(Panel(status_text, border_style="dim"))
time.sleep(0.1)
except KeyboardInterrupt:
console.print("\n[dim]Processing interrupted[/dim]")
# Wait for thread to finish
bg_thread.join(timeout=1.0)
# Handle result
if result_container['error']:
console.print(f"\n[red]Error: {result_container['error']}[/red]")
elif result_container['response']:
# Print response with streaming effect
console.print()
response_str = result_container['response']
words = response_str.split()
import sys
for i, word in enumerate(words):
console.print(word + " ", end="")
sys.stdout.flush()
if i % 15 == 14:
time.sleep(0.005)
console.print()
# Update session state
input_tokens = len(prompt) // 4
output_tokens = len(response_str) // 4
session_state['total_input_tokens'] += input_tokens
session_state['total_output_tokens'] += output_tokens
session_state['request_count'] += 1
session_state['conversation_history'].append({'role': 'user', 'content': prompt})
session_state['conversation_history'].append({'role': 'assistant', 'content': response_str})
# Set state back to idle
state_manager.set_state(ProcessingState.IDLE)
live_status.clear()
# Process next queued message if any
next_msg = message_queue.pop()
if next_msg:
console.print(f"\n[dim cyan]Processing queued message...[/dim cyan]")
self._process_interactive_prompt_async(next_msg, tools_list, console, session_state)
def _process_interactive_prompt(self, prompt, tools_list, console, show_profiling=False, session_state=None):
"""Process a prompt in interactive mode with streaming."""
from rich.live import Live
from rich.spinner import Spinner
import sys
import warnings
import logging
import time
# Profiling timestamps
timings = {}
timings['start'] = time.time()
# Store original log levels to restore later (no global impact)
original_levels = {}
loggers_to_suppress = ["httpx", "httpcore", "duckduckgo_search", "crawl4ai", "lib"]
for logger_name in loggers_to_suppress:
logger = logging.getLogger(logger_name)
original_levels[logger_name] = logger.level
logger.setLevel(logging.WARNING)
# Temporarily filter warnings (scoped to this function)
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message=".*duckduckgo_search.*")
warnings.filterwarnings("ignore", message=".*has been renamed.*")
try:
# Import agent
timings['import_start'] = time.time()
from praisonaiagents import Agent
timings['import_end'] = time.time()
# Determine model to use
model = None
if session_state and session_state.get('current_model'):
model = session_state['current_model']
elif hasattr(self, 'args') and getattr(self.args, 'llm', None):
model = self.args.llm
# Show thinking indicator and create agent
timings['agent_create_start'] = time.time()
with Live(Spinner("dots", text="Thinking...", style="cyan"), console=console, refresh_per_second=10, transient=True):
# Create agent with tools
agent = Agent(
name="Assistant",
role="Helpful AI Assistant",
goal="Help the user with their tasks",
backstory="You are a helpful AI assistant with access to tools for file operations, shell commands, and web search. Use tools when needed to complete tasks.",
tools=tools_list if tools_list else None,
verbose=False, # Suppress verbose panels
llm=model
)
timings['agent_create_end'] = time.time()
# Get response
console.print() # New line before response
timings['llm_start'] = time.time()
# Use chat method (streaming is handled internally by verbose mode)
response = agent.chat(prompt, stream=False)
timings['llm_end'] = time.time()
# Check if tools were used by looking at agent's tool execution history
if hasattr(agent, '_tool_calls') and agent._tool_calls:
for tool_call in agent._tool_calls:
tool_name = tool_call.get('name', 'unknown')
console.print(f"[dim]⚙ Used tool: {tool_name}[/dim]")
# Print response with simulated streaming effect
timings['display_start'] = time.time()
response_str = str(response) if response else ""
if response_str:
words = response_str.split()
for i, word in enumerate(words):
console.print(word + " ", end="")
sys.stdout.flush()
if i % 15 == 14: # Small pause every 15 words for streaming effect
time.sleep(0.005)
console.print() # Final newline
timings['display_end'] = time.time()
# Update session state with token estimates and history
if session_state is not None:
# Estimate tokens (rough: ~4 chars per token)
input_tokens = len(prompt) // 4
output_tokens = len(response_str) // 4
session_state['total_input_tokens'] += input_tokens
session_state['total_output_tokens'] += output_tokens
session_state['request_count'] += 1
# Add to conversation history
session_state['conversation_history'].append({
'role': 'user',
'content': prompt
})
session_state['conversation_history'].append({
'role': 'assistant',
'content': response_str
})
# Show profiling if enabled
if show_profiling:
timings['total'] = time.time() - timings['start']
console.print("\n[dim]─── Profiling ───[/dim]")
console.print(f"[dim]Import: {(timings['import_end'] - timings['import_start'])*1000:.1f}ms[/dim]")
console.print(f"[dim]Agent setup: {(timings['agent_create_end'] - timings['agent_create_start'])*1000:.1f}ms[/dim]")
console.print(f"[dim]LLM call: {(timings['llm_end'] - timings['llm_start'])*1000:.1f}ms[/dim]")
console.print(f"[dim]Display: {(timings['display_end'] - timings['display_start'])*1000:.1f}ms[/dim]")
console.print(f"[dim]Total: {timings['total']*1000:.1f}ms[/dim]")
except Exception as e:
console.print(f"\n[red]Error: {e}[/red]")
import traceback
traceback.print_exc()
finally:
# Restore original log levels (no global impact on main package)
for logger_name, level in original_levels.items():
logging.getLogger(logger_name).setLevel(level)
if __name__ == "__main__":
praison_ai = PraisonAI()
+1 -1
View File
@@ -57,7 +57,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==2.3.48 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.3.52 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.3.48"
__version__ = "2.3.52"
@@ -0,0 +1,151 @@
"""
Unit tests for --chat flag (alias for --chat-mode).
Tests verify that:
1. --chat flag works as an alias for --chat-mode
2. --chat flag does not conflict with 'chat' subcommand
3. Both flags set chat_mode=True correctly
4. Backward compatibility with --chat-mode is maintained
"""
import sys
import os
import argparse
# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))))
class TestChatFlagParsing:
"""Test argument parsing for --chat and --chat-mode flags."""
def test_chat_mode_flag_sets_chat_mode_true(self):
"""--chat-mode should set chat_mode=True."""
parser = argparse.ArgumentParser()
parser.add_argument("--chat-mode", "--chat", action="store_true", dest="chat_mode")
parser.add_argument("command", nargs="?")
args, _ = parser.parse_known_args(["--chat-mode", "test prompt"])
assert args.chat_mode is True
assert args.command == "test prompt"
def test_chat_flag_sets_chat_mode_true(self):
"""--chat should set chat_mode=True (alias for --chat-mode)."""
parser = argparse.ArgumentParser()
parser.add_argument("--chat-mode", "--chat", action="store_true", dest="chat_mode")
parser.add_argument("command", nargs="?")
args, _ = parser.parse_known_args(["--chat", "test prompt"])
assert args.chat_mode is True
assert args.command == "test prompt"
def test_chat_subcommand_does_not_set_chat_mode(self):
"""'chat' as positional command should NOT set chat_mode."""
parser = argparse.ArgumentParser()
parser.add_argument("--chat-mode", "--chat", action="store_true", dest="chat_mode")
parser.add_argument("command", nargs="?")
args, _ = parser.parse_known_args(["chat"])
assert args.chat_mode is False
assert args.command == "chat"
def test_chat_flag_after_prompt(self):
"""--chat after prompt should work."""
parser = argparse.ArgumentParser()
parser.add_argument("--chat-mode", "--chat", action="store_true", dest="chat_mode")
parser.add_argument("command", nargs="?")
args, _ = parser.parse_known_args(["What is 2+2?", "--chat"])
assert args.chat_mode is True
assert args.command == "What is 2+2?"
def test_chat_mode_flag_after_prompt(self):
"""--chat-mode after prompt should work (backward compat)."""
parser = argparse.ArgumentParser()
parser.add_argument("--chat-mode", "--chat", action="store_true", dest="chat_mode")
parser.add_argument("command", nargs="?")
args, _ = parser.parse_known_args(["What is 2+2?", "--chat-mode"])
assert args.chat_mode is True
assert args.command == "What is 2+2?"
def test_no_flag_defaults_to_false(self):
"""Without --chat or --chat-mode, chat_mode should be False."""
parser = argparse.ArgumentParser()
parser.add_argument("--chat-mode", "--chat", action="store_true", dest="chat_mode")
parser.add_argument("command", nargs="?")
args, _ = parser.parse_known_args(["some prompt"])
assert args.chat_mode is False
assert args.command == "some prompt"
class TestChatFlagNoConflict:
"""Test that --chat flag does not conflict with other features."""
def test_chat_flag_with_other_flags(self):
"""--chat should work alongside other flags."""
parser = argparse.ArgumentParser()
parser.add_argument("--chat-mode", "--chat", action="store_true", dest="chat_mode")
parser.add_argument("--verbose", "-v", action="store_true")
parser.add_argument("--model", type=str)
parser.add_argument("command", nargs="?")
args, _ = parser.parse_known_args(["--chat", "--verbose", "--model", "gpt-4o", "test"])
assert args.chat_mode is True
assert args.verbose is True
assert args.model == "gpt-4o"
assert args.command == "test"
def test_chat_subcommand_with_other_flags(self):
"""'chat' subcommand should work with other flags."""
parser = argparse.ArgumentParser()
parser.add_argument("--chat-mode", "--chat", action="store_true", dest="chat_mode")
parser.add_argument("--verbose", "-v", action="store_true")
parser.add_argument("command", nargs="?")
args, _ = parser.parse_known_args(["chat", "--verbose"])
assert args.chat_mode is False # subcommand, not flag
assert args.command == "chat"
assert args.verbose is True
class TestChatFlagIntegration:
"""Integration tests with actual PraisonAI CLI parser."""
def test_praisonai_parser_has_chat_flag(self):
"""PraisonAI CLI should have --chat as alias for --chat-mode."""
# This test will fail until we implement the change
from praisonai.cli.main import PraisonAI
# Create instance and get parser
pai = PraisonAI()
# Mock sys.argv to avoid test environment issues
import sys
original_argv = sys.argv
try:
sys.argv = ["praisonai", "--chat", "test prompt"]
args = pai.parse_args()
assert hasattr(args, 'chat_mode')
# After implementation, this should be True
# For now, it may work due to argparse prefix matching
finally:
sys.argv = original_argv
def test_praisonai_chat_subcommand_still_works(self):
"""'praisonai chat' subcommand should still launch Chainlit.
Note: In test environment, parse_args returns default args to avoid
pytest interference. This test verifies the parser structure instead.
"""
# Test the parser structure directly without going through PraisonAI
# which has test environment detection
parser = argparse.ArgumentParser()
parser.add_argument("--chat-mode", "--chat", action="store_true", dest="chat_mode")
parser.add_argument("command", nargs="?")
# Simulate 'praisonai chat' - should set command='chat', not chat_mode
args, _ = parser.parse_known_args(["chat"])
assert args.command == "chat"
assert args.chat_mode is False # subcommand, not flag
@@ -0,0 +1,661 @@
"""
Unit tests for Message Queue feature.
Tests for:
1. MessageQueue - FIFO queue for messages
2. ProcessingState - State management
3. QueuedMessage - Message with metadata
4. MessageQueueHandler - Main handler class
"""
import time
import threading
# ============================================================================
# 1. MessageQueue Tests - FIFO operations
# ============================================================================
class TestMessageQueue:
"""Test MessageQueue FIFO operations."""
def test_create_empty_queue(self):
"""Queue should start empty."""
from praisonai.cli.features.message_queue import MessageQueue
queue = MessageQueue()
assert queue.is_empty
assert queue.count == 0
def test_add_message(self):
"""Should add message to queue."""
from praisonai.cli.features.message_queue import MessageQueue
queue = MessageQueue()
queue.add("Hello")
assert not queue.is_empty
assert queue.count == 1
def test_add_multiple_messages(self):
"""Should add multiple messages in order."""
from praisonai.cli.features.message_queue import MessageQueue
queue = MessageQueue()
queue.add("First")
queue.add("Second")
queue.add("Third")
assert queue.count == 3
def test_pop_message_fifo(self):
"""Should pop messages in FIFO order."""
from praisonai.cli.features.message_queue import MessageQueue
queue = MessageQueue()
queue.add("First")
queue.add("Second")
msg = queue.pop()
assert msg == "First"
assert queue.count == 1
def test_pop_empty_queue(self):
"""Should return None when popping empty queue."""
from praisonai.cli.features.message_queue import MessageQueue
queue = MessageQueue()
msg = queue.pop()
assert msg is None
def test_peek_message(self):
"""Should peek without removing."""
from praisonai.cli.features.message_queue import MessageQueue
queue = MessageQueue()
queue.add("First")
queue.add("Second")
msg = queue.peek()
assert msg == "First"
assert queue.count == 2 # Not removed
def test_peek_empty_queue(self):
"""Should return None when peeking empty queue."""
from praisonai.cli.features.message_queue import MessageQueue
queue = MessageQueue()
msg = queue.peek()
assert msg is None
def test_clear_queue(self):
"""Should clear all messages."""
from praisonai.cli.features.message_queue import MessageQueue
queue = MessageQueue()
queue.add("First")
queue.add("Second")
queue.clear()
assert queue.is_empty
assert queue.count == 0
def test_get_all_messages(self):
"""Should get all messages as list."""
from praisonai.cli.features.message_queue import MessageQueue
queue = MessageQueue()
queue.add("First")
queue.add("Second")
messages = queue.get_all()
assert messages == ["First", "Second"]
assert queue.count == 2 # Not removed
def test_remove_at_index(self):
"""Should remove message at specific index."""
from praisonai.cli.features.message_queue import MessageQueue
queue = MessageQueue()
queue.add("First")
queue.add("Second")
queue.add("Third")
removed = queue.remove_at(1)
assert removed == "Second"
assert queue.count == 2
assert queue.get_all() == ["First", "Third"]
def test_remove_at_invalid_index(self):
"""Should return None for invalid index."""
from praisonai.cli.features.message_queue import MessageQueue
queue = MessageQueue()
queue.add("First")
removed = queue.remove_at(5)
assert removed is None
assert queue.count == 1
def test_add_empty_message_ignored(self):
"""Should ignore empty messages."""
from praisonai.cli.features.message_queue import MessageQueue
queue = MessageQueue()
queue.add("")
queue.add(" ")
assert queue.is_empty
def test_thread_safety(self):
"""Queue should be thread-safe."""
from praisonai.cli.features.message_queue import MessageQueue
queue = MessageQueue()
results = []
def add_messages():
for i in range(100):
queue.add(f"msg-{i}")
def pop_messages():
for _ in range(100):
msg = queue.pop()
if msg:
results.append(msg)
t1 = threading.Thread(target=add_messages)
t2 = threading.Thread(target=pop_messages)
t1.start()
t2.start()
t1.join()
t2.join()
# All messages should be accounted for
total = len(results) + queue.count
assert total == 100
# ============================================================================
# 2. ProcessingState Tests
# ============================================================================
class TestProcessingState:
"""Test ProcessingState enum and state management."""
def test_state_enum_values(self):
"""Should have correct state values."""
from praisonai.cli.features.message_queue import ProcessingState
assert ProcessingState.IDLE.value == "idle"
assert ProcessingState.PROCESSING.value == "processing"
assert ProcessingState.WAITING_APPROVAL.value == "waiting_approval"
def test_state_manager_initial_state(self):
"""Should start in IDLE state."""
from praisonai.cli.features.message_queue import StateManager, ProcessingState
manager = StateManager()
assert manager.current_state == ProcessingState.IDLE
def test_state_transition(self):
"""Should transition between states."""
from praisonai.cli.features.message_queue import StateManager, ProcessingState
manager = StateManager()
manager.set_state(ProcessingState.PROCESSING)
assert manager.current_state == ProcessingState.PROCESSING
def test_is_idle(self):
"""Should correctly report idle state."""
from praisonai.cli.features.message_queue import StateManager, ProcessingState
manager = StateManager()
assert manager.is_idle
manager.set_state(ProcessingState.PROCESSING)
assert not manager.is_idle
def test_is_processing(self):
"""Should correctly report processing state."""
from praisonai.cli.features.message_queue import StateManager, ProcessingState
manager = StateManager()
assert not manager.is_processing
manager.set_state(ProcessingState.PROCESSING)
assert manager.is_processing
def test_state_callback(self):
"""Should call callback on state change."""
from praisonai.cli.features.message_queue import StateManager, ProcessingState
callback_called = []
def on_state_change(old_state, new_state):
callback_called.append((old_state, new_state))
manager = StateManager(on_state_change=on_state_change)
manager.set_state(ProcessingState.PROCESSING)
assert len(callback_called) == 1
assert callback_called[0] == (ProcessingState.IDLE, ProcessingState.PROCESSING)
# ============================================================================
# 3. QueuedMessage Tests
# ============================================================================
class TestQueuedMessage:
"""Test QueuedMessage dataclass."""
def test_create_queued_message(self):
"""Should create message with content."""
from praisonai.cli.features.message_queue import QueuedMessage
msg = QueuedMessage(content="Hello")
assert msg.content == "Hello"
assert msg.timestamp is not None
def test_message_has_timestamp(self):
"""Should have creation timestamp."""
from praisonai.cli.features.message_queue import QueuedMessage
before = time.time()
msg = QueuedMessage(content="Hello")
after = time.time()
assert before <= msg.timestamp <= after
def test_message_priority(self):
"""Should support priority levels."""
from praisonai.cli.features.message_queue import QueuedMessage, MessagePriority
msg = QueuedMessage(content="Hello", priority=MessagePriority.HIGH)
assert msg.priority == MessagePriority.HIGH
def test_default_priority_normal(self):
"""Should default to NORMAL priority."""
from praisonai.cli.features.message_queue import QueuedMessage, MessagePriority
msg = QueuedMessage(content="Hello")
assert msg.priority == MessagePriority.NORMAL
# ============================================================================
# 4. MessageQueueHandler Tests
# ============================================================================
class TestMessageQueueHandler:
"""Test MessageQueueHandler main class."""
def test_handler_creation(self):
"""Should create handler with default settings."""
from praisonai.cli.features.message_queue import MessageQueueHandler
handler = MessageQueueHandler()
assert handler.queue.is_empty
assert handler.state_manager.is_idle
def test_submit_when_idle(self):
"""Should process immediately when idle."""
from praisonai.cli.features.message_queue import MessageQueueHandler
processed = []
def processor(msg):
processed.append(msg)
return f"Response to: {msg}"
handler = MessageQueueHandler(processor=processor)
handler.submit("Hello")
# Should process immediately
assert "Hello" in processed
def test_queue_when_processing(self):
"""Should queue message when already processing."""
from praisonai.cli.features.message_queue import MessageQueueHandler, ProcessingState
handler = MessageQueueHandler()
handler.state_manager.set_state(ProcessingState.PROCESSING)
handler.submit("Hello")
# Should be queued, not processed
assert handler.queue.count == 1
def test_auto_process_queue_on_idle(self):
"""Should auto-process queue when becoming idle."""
from praisonai.cli.features.message_queue import MessageQueueHandler, ProcessingState
processed = []
def processor(msg):
processed.append(msg)
return f"Response: {msg}"
handler = MessageQueueHandler(processor=processor)
# Simulate processing state
handler.state_manager.set_state(ProcessingState.PROCESSING)
handler.submit("Queued1")
handler.submit("Queued2")
assert handler.queue.count == 2
# Simulate becoming idle - should trigger queue processing
handler.on_processing_complete()
# Queue should be processed
assert "Queued1" in processed or handler.queue.count < 2
def test_get_queue_status(self):
"""Should return queue status."""
from praisonai.cli.features.message_queue import MessageQueueHandler, ProcessingState
handler = MessageQueueHandler()
handler.state_manager.set_state(ProcessingState.PROCESSING)
handler.submit("Hello")
handler.submit("World")
status = handler.get_status()
assert status['queue_count'] == 2
assert status['state'] == 'processing'
def test_clear_queue(self):
"""Should clear queue via handler."""
from praisonai.cli.features.message_queue import MessageQueueHandler, ProcessingState
handler = MessageQueueHandler()
handler.state_manager.set_state(ProcessingState.PROCESSING)
handler.submit("Hello")
handler.clear_queue()
assert handler.queue.is_empty
# ============================================================================
# 5. QueueDisplay Tests
# ============================================================================
class TestQueueDisplay:
"""Test QueueDisplay for visual indicators."""
def test_format_empty_queue(self):
"""Should return empty string for empty queue."""
from praisonai.cli.features.message_queue import QueueDisplay, MessageQueue
queue = MessageQueue()
display = QueueDisplay(queue)
assert display.format_queue() == ""
def test_format_single_message(self):
"""Should format single message."""
from praisonai.cli.features.message_queue import QueueDisplay, MessageQueue
queue = MessageQueue()
queue.add("Hello world")
display = QueueDisplay(queue)
formatted = display.format_queue()
assert "↳" in formatted
assert "Hello world" in formatted
def test_format_multiple_messages(self):
"""Should format multiple messages."""
from praisonai.cli.features.message_queue import QueueDisplay, MessageQueue
queue = MessageQueue()
queue.add("First")
queue.add("Second")
display = QueueDisplay(queue)
formatted = display.format_queue()
assert "First" in formatted
assert "Second" in formatted
def test_truncate_long_message(self):
"""Should truncate long messages."""
from praisonai.cli.features.message_queue import QueueDisplay, MessageQueue
queue = MessageQueue()
queue.add("A" * 100)
display = QueueDisplay(queue, max_message_length=20)
formatted = display.format_queue()
assert "..." in formatted
def test_format_status_idle(self):
"""Should show idle status."""
from praisonai.cli.features.message_queue import QueueDisplay, MessageQueue, StateManager
queue = MessageQueue()
state = StateManager()
display = QueueDisplay(queue, state_manager=state)
status = display.format_status()
assert "idle" in status.lower() or status == ""
def test_format_status_processing(self):
"""Should show processing status."""
from praisonai.cli.features.message_queue import (
QueueDisplay, MessageQueue, StateManager, ProcessingState
)
queue = MessageQueue()
state = StateManager()
state.set_state(ProcessingState.PROCESSING)
display = QueueDisplay(queue, state_manager=state)
status = display.format_status()
assert "processing" in status.lower() or "⏳" in status
def test_format_queue_count(self):
"""Should show queue count."""
from praisonai.cli.features.message_queue import QueueDisplay, MessageQueue, ProcessingState, StateManager
queue = MessageQueue()
queue.add("First")
queue.add("Second")
state = StateManager()
state.set_state(ProcessingState.PROCESSING)
display = QueueDisplay(queue, state_manager=state)
count_str = display.format_queue_count()
assert "2" in count_str
# ============================================================================
# 6. Integration Tests
# ============================================================================
class TestMessageQueueIntegration:
"""Integration tests for message queue system."""
def test_full_workflow(self):
"""Test complete queue workflow."""
from praisonai.cli.features.message_queue import (
MessageQueueHandler, ProcessingState
)
results = []
def processor(msg):
results.append(msg)
return f"Processed: {msg}"
handler = MessageQueueHandler(processor=processor)
# 1. Submit when idle - processes immediately
handler.submit("First")
assert "First" in results
# 2. Simulate processing
handler.state_manager.set_state(ProcessingState.PROCESSING)
# 3. Queue messages while processing
handler.submit("Second")
handler.submit("Third")
assert handler.queue.count == 2
# 4. Complete processing - should auto-process queue
handler.on_processing_complete()
# Queue should be processed
assert handler.queue.count < 2 or "Second" in results
def test_queue_with_display(self):
"""Test queue with display formatting."""
from praisonai.cli.features.message_queue import (
MessageQueueHandler, QueueDisplay, ProcessingState
)
handler = MessageQueueHandler()
handler.state_manager.set_state(ProcessingState.PROCESSING)
handler.submit("Task 1")
handler.submit("Task 2")
display = QueueDisplay(handler.queue, state_manager=handler.state_manager)
# Should show processing status
status = display.format_status()
assert "processing" in status.lower() or "⏳" in status
# Should show queued messages
queue_str = display.format_queue()
assert "Task 1" in queue_str
assert "Task 2" in queue_str
# ============================================================================
# 9. AsyncProcessor Tests - Background processing
# ============================================================================
class TestAsyncProcessor:
"""Test AsyncProcessor for background agent execution."""
def test_processor_creation(self):
"""Should create processor with callback."""
from praisonai.cli.features.message_queue import AsyncProcessor
results = []
def on_complete(result):
results.append(result)
processor = AsyncProcessor(on_complete=on_complete)
assert processor is not None
def test_start_processing(self):
"""Should start processing in background thread."""
from praisonai.cli.features.message_queue import AsyncProcessor
import time
results = []
def work_fn():
time.sleep(0.05)
return "done"
def on_complete(result):
results.append(result)
processor = AsyncProcessor(on_complete=on_complete)
processor.start(work_fn)
# Should be processing
assert processor.is_running
# Wait for completion
time.sleep(0.15)
assert not processor.is_running
assert "done" in results
def test_is_running_property(self):
"""Should track running state."""
from praisonai.cli.features.message_queue import AsyncProcessor
import time
def slow_work():
time.sleep(0.1)
return "result"
processor = AsyncProcessor()
assert not processor.is_running
processor.start(slow_work)
assert processor.is_running
time.sleep(0.15)
assert not processor.is_running
def test_on_status_callback(self):
"""Should call status callback during processing."""
from praisonai.cli.features.message_queue import AsyncProcessor
import time
statuses = []
def on_status(status):
statuses.append(status)
def work_with_status(status_callback):
status_callback("Starting...")
time.sleep(0.02)
status_callback("Working...")
return "done"
processor = AsyncProcessor(on_status=on_status)
processor.start(lambda: work_with_status(on_status))
time.sleep(0.1)
assert len(statuses) >= 1
# ============================================================================
# 10. LiveStatusDisplay Tests - Real-time status
# ============================================================================
class TestLiveStatusDisplay:
"""Test LiveStatusDisplay for real-time tool/command status."""
def test_display_creation(self):
"""Should create display."""
from praisonai.cli.features.message_queue import LiveStatusDisplay
display = LiveStatusDisplay()
assert display is not None
def test_update_status(self):
"""Should update current status."""
from praisonai.cli.features.message_queue import LiveStatusDisplay
display = LiveStatusDisplay()
display.update_status("Processing...")
assert display.current_status == "Processing..."
def test_add_tool_call(self):
"""Should track tool calls."""
from praisonai.cli.features.message_queue import LiveStatusDisplay
display = LiveStatusDisplay()
display.add_tool_call("read_file", {"path": "test.py"})
assert len(display.tool_calls) == 1
assert display.tool_calls[0]['name'] == "read_file"
def test_add_command_execution(self):
"""Should track command executions."""
from praisonai.cli.features.message_queue import LiveStatusDisplay
display = LiveStatusDisplay()
display.add_command("ls -la")
assert len(display.commands) == 1
assert display.commands[0] == "ls -la"
def test_format_live_status(self):
"""Should format live status for display."""
from praisonai.cli.features.message_queue import LiveStatusDisplay
display = LiveStatusDisplay()
display.update_status("Thinking...")
display.add_tool_call("read_file", {"path": "main.py"})
formatted = display.format()
assert "Thinking" in formatted or "read_file" in formatted
def test_clear_status(self):
"""Should clear all status."""
from praisonai.cli.features.message_queue import LiveStatusDisplay
display = LiveStatusDisplay()
display.update_status("Working")
display.add_tool_call("test", {})
display.add_command("echo hi")
display.clear()
assert display.current_status == ""
assert len(display.tool_calls) == 0
assert len(display.commands) == 0
# ============================================================================
# 11. NonBlockingInput Tests - Async input handling
# ============================================================================
class TestNonBlockingInput:
"""Test NonBlockingInput for async user input."""
def test_input_handler_creation(self):
"""Should create input handler."""
from praisonai.cli.features.message_queue import NonBlockingInput
handler = NonBlockingInput()
assert handler is not None
def test_submit_input(self):
"""Should accept input while processing."""
from praisonai.cli.features.message_queue import NonBlockingInput
handler = NonBlockingInput()
handler.submit("new message")
assert handler.has_pending
assert handler.pop() == "new message"
def test_multiple_inputs(self):
"""Should queue multiple inputs."""
from praisonai.cli.features.message_queue import NonBlockingInput
handler = NonBlockingInput()
handler.submit("first")
handler.submit("second")
assert handler.pending_count == 2
assert handler.pop() == "first"
assert handler.pop() == "second"