Compare commits

...
4 Commits
Author SHA1 Message Date
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
12 changed files with 1697 additions and 143 deletions

No files matched your search

+84 -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,31 @@ 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)
praisonai "list files in current folder" --chat-mode
praisonai "search the web for AI news" --chat-mode
praisonai "read README.md" --chat-mode
```
### 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 +1471,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 +1800,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.50" \
"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.50" \
"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.50" \
"praisonai[ui]" \
"praisonai[crewai]"
+84 -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,31 @@ 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)
praisonai "list files in current folder" --chat-mode
praisonai "search the web for AI news" --chat-mode
praisonai "read README.md" --chat-mode
```
### 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 +1471,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 +1800,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.50.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.50.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,395 @@
"""
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
# ============================================================================
# Exports
# ============================================================================
__all__ = [
'ProcessingState',
'MessagePriority',
'QueuedMessage',
'MessageQueue',
'StateManager',
'QueueDisplay',
'MessageQueueHandler',
]
+649 -54
View File
@@ -214,6 +214,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 +704,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", 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 +3773,664 @@ 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
)
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)
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,
'processing_lock': processing_lock,
}
# 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)
# Process the prompt with profiling if enabled
self._process_interactive_prompt(
processed_input,
tools_list,
console,
show_profiling=session_state['show_profiling'],
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
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(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("\n") # 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.50 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.50"
@@ -0,0 +1,473 @@
"""
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