mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 17:31:00 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5b768c213 | ||
|
|
068bdbebc5 | ||
|
|
4d80d795f8 | ||
|
|
2e3b5cd29b | ||
|
|
d948b6de2c | ||
|
|
3adb06950c | ||
|
|
e45e85975c | ||
|
|
8a7935dc2c | ||
|
|
e22fda805f | ||
|
|
2cde2fd731 | ||
|
|
f7534681e5 | ||
|
|
8c9e3e34cb | ||
|
|
c470494d76 | ||
|
|
fdf3cac3e7 | ||
|
|
ec98cd12c8 | ||
|
|
f318ef02a9 | ||
|
|
f086566a2d | ||
|
|
6de011b69e | ||
|
|
89d7821bc9 | ||
|
|
9bff7eddbd | ||
|
|
82e5f11acf |
No files matched your search
@@ -0,0 +1,60 @@
|
||||
name: Run tests and upload coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests and collect coverage
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
- name: Install UV
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/praisonai
|
||||
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
uv pip install --system duckduckgo_search
|
||||
uv pip install --system pytest pytest-cov pytest-asyncio
|
||||
# Install knowledge dependencies from praisonai-agents
|
||||
uv pip install --system "praisonaiagents[knowledge]"
|
||||
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_MODEL_NAME=${{ secrets.OPENAI_MODEL_NAME || 'gpt-4o-mini' }}" >> $GITHUB_ENV
|
||||
echo "LOGLEVEL=DEBUG" >> $GITHUB_ENV
|
||||
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd src/praisonai
|
||||
pytest --cov=praisonai --cov-branch --cov-report=xml tests/unit/
|
||||
|
||||
- name: Upload results to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: src/praisonai/coverage.xml
|
||||
flags: unit-tests
|
||||
name: unit-tests-coverage
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
@@ -65,4 +65,4 @@ jobs:
|
||||
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
cd src/praisonai && python -m pytest
|
||||
cd src/praisonai && python tests/test_runner.py --pattern fast
|
||||
@@ -69,10 +69,10 @@ jobs:
|
||||
|
||||
case $TEST_TYPE in
|
||||
"unit")
|
||||
cd src/praisonai && python tests/test_runner.py --pattern unit
|
||||
cd src/praisonai && python -m pytest tests/unit/ -v --tb=short --disable-warnings --cov=praisonai --cov-report=xml --cov-branch
|
||||
;;
|
||||
"integration")
|
||||
cd src/praisonai && python tests/test_runner.py --pattern integration
|
||||
cd src/praisonai && python -m pytest tests/integration/ -v --tb=short --disable-warnings --cov=praisonai --cov-report=xml --cov-branch
|
||||
;;
|
||||
"fast")
|
||||
cd src/praisonai && python tests/test_runner.py --pattern fast
|
||||
@@ -139,6 +139,16 @@ jobs:
|
||||
echo "- 🔌 MCP server connections" >> comprehensive_report.md
|
||||
echo "- 💬 LLM integrations (OpenAI, Anthropic, etc.)" >> comprehensive_report.md
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: src/praisonai/coverage.xml
|
||||
flags: comprehensive-tests
|
||||
name: comprehensive-tests-coverage
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
- name: Upload Comprehensive Test Results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
@@ -146,9 +156,9 @@ jobs:
|
||||
name: comprehensive-test-results-python-${{ matrix.python-version }}
|
||||
path: |
|
||||
comprehensive_report.md
|
||||
htmlcov/
|
||||
coverage.xml
|
||||
.coverage
|
||||
src/praisonai/htmlcov/
|
||||
src/praisonai/coverage.xml
|
||||
src/praisonai/.coverage
|
||||
retention-days: 30
|
||||
|
||||
test-matrix-summary:
|
||||
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: |
|
||||
cd src/praisonai && python -m pytest tests/unit/ -v --tb=short --disable-warnings --cov=praisonai --cov-report=term-missing
|
||||
cd src/praisonai && python -m pytest tests/unit/ -v --tb=short --disable-warnings --cov=praisonai --cov-report=term-missing --cov-report=xml --cov-branch
|
||||
|
||||
- name: Run Integration Tests
|
||||
run: |
|
||||
@@ -263,12 +263,24 @@ jobs:
|
||||
export OPENAI_MODEL_NAME="$OPENAI_MODEL_NAME"
|
||||
cd src/praisonai && python -m pytest tests/test.py -v --tb=short --disable-warnings
|
||||
|
||||
- name: Upload Coverage Reports
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: matrix.python-version == '3.11'
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: src/praisonai/coverage.xml
|
||||
flags: core-tests
|
||||
name: core-tests-coverage
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
- name: Upload Coverage Reports (Artifacts)
|
||||
if: matrix.python-version == '3.11'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-reports
|
||||
path: |
|
||||
.coverage
|
||||
htmlcov/
|
||||
src/praisonai/.coverage
|
||||
src/praisonai/htmlcov/
|
||||
src/praisonai/coverage.xml
|
||||
retention-days: 7
|
||||
@@ -471,8 +471,8 @@ jobs:
|
||||
|
||||
- name: Run Fast Tests
|
||||
run: |
|
||||
# Run the fastest, most essential tests
|
||||
cd src/praisonai && python tests/test_runner.py --pattern fast
|
||||
# Run the fastest, most essential tests with coverage
|
||||
cd src/praisonai && python -m pytest tests/unit/test_core_agents.py -v --tb=short --disable-warnings --cov=praisonai --cov-report=xml --cov-branch
|
||||
|
||||
- name: Run Real API Tests
|
||||
run: |
|
||||
@@ -491,6 +491,16 @@ jobs:
|
||||
cd src/praisonai && python -m pytest tests/test.py -v --tb=short --disable-warnings
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: src/praisonai/coverage.xml
|
||||
flags: quick-validation
|
||||
name: quick-validation-coverage
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
- name: Restore Root Config Files
|
||||
run: |
|
||||
echo "🔄 Restoring root configuration files..."
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
flask \
|
||||
"praisonai>=2.2.26" \
|
||||
"praisonai>=2.2.27" \
|
||||
"praisonai[api]" \
|
||||
gunicorn \
|
||||
markdown
|
||||
|
||||
@@ -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.2.26" \
|
||||
"praisonai>=2.2.27" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -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.2.26" \
|
||||
"praisonai>=2.2.27" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -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.2.26" \
|
||||
"praisonai>=2.2.27" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
+2
-2
@@ -121,7 +121,7 @@ healthcheck:
|
||||
## 📦 Package Versions
|
||||
|
||||
All Docker images use consistent, up-to-date versions:
|
||||
- PraisonAI: `>=2.2.26`
|
||||
- PraisonAI: `>=2.2.27`
|
||||
- PraisonAI Agents: `>=0.0.92`
|
||||
- Python: `3.11-slim`
|
||||
|
||||
@@ -218,7 +218,7 @@ docker-compose up -d
|
||||
### Version Pinning
|
||||
To use specific versions, update the Dockerfile:
|
||||
```dockerfile
|
||||
RUN pip install "praisonai==2.2.26" "praisonaiagents==0.0.92"
|
||||
RUN pip install "praisonai==2.2.27" "praisonaiagents==0.0.92"
|
||||
```
|
||||
|
||||
## 🌐 Production Deployment
|
||||
|
||||
@@ -0,0 +1,459 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "bLeMxwWimvzX"
|
||||
},
|
||||
"source": [
|
||||
"# Code Analysis Agent\n",
|
||||
"Learn how to create AI agents for comprehensive code analysis and quality assessment."
|
||||
],
|
||||
"id": "bLeMxwWimvzX"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"[](https://colab.research.google.com/github/DhivyaBharathy-web/PraisonAI/blob/main/examples/cookbooks/code_analysis_agent.ipynb)\n"
|
||||
],
|
||||
"metadata": {
|
||||
"id": "P3X8PXPyojnL"
|
||||
},
|
||||
"id": "P3X8PXPyojnL"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "xEs8bwWfmvza"
|
||||
},
|
||||
"source": [
|
||||
"## Dependencies\n",
|
||||
"Install the required packages."
|
||||
],
|
||||
"id": "xEs8bwWfmvza"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "_RPiU3j6mvzb"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install praisonaiagents gitingest"
|
||||
],
|
||||
"id": "_RPiU3j6mvzb"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "81-orTkzmvzc"
|
||||
},
|
||||
"source": [
|
||||
"## Set API Key\n",
|
||||
"Set your OpenAI API key as an environment variable in your terminal:"
|
||||
],
|
||||
"id": "81-orTkzmvzc"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "84iL7rGimvzc"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"os.environ['OPENAI_API_KEY'] = 'your_api_key_here'"
|
||||
],
|
||||
"id": "84iL7rGimvzc"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "TV15GFsRmvzc"
|
||||
},
|
||||
"source": [
|
||||
"## Tools and Data Models\n",
|
||||
"Define the agent, task, and output schema."
|
||||
],
|
||||
"id": "TV15GFsRmvzc"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "0Wq_g0pNmvzd"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from praisonaiagents import Agent, Task, PraisonAIAgents\n",
|
||||
"from pydantic import BaseModel\n",
|
||||
"from typing import List, Dict\n",
|
||||
"from gitingest import ingest\n",
|
||||
"\n",
|
||||
"class CodeMetrics(BaseModel):\n",
|
||||
" category: str\n",
|
||||
" score: int\n",
|
||||
" findings: List[str]\n",
|
||||
"\n",
|
||||
"class CodeAnalysisReport(BaseModel):\n",
|
||||
" overall_quality: int\n",
|
||||
" code_metrics: List[CodeMetrics]\n",
|
||||
" architecture_score: int\n",
|
||||
" maintainability_score: int\n",
|
||||
" performance_score: int\n",
|
||||
" security_score: int\n",
|
||||
" test_coverage: int\n",
|
||||
" key_strengths: List[str]\n",
|
||||
" improvement_areas: List[str]\n",
|
||||
" tech_stack: List[str]\n",
|
||||
" recommendations: List[str]\n",
|
||||
" complexity_metrics: Dict[str, int]\n",
|
||||
" best_practices: List[Dict[str, str]]\n",
|
||||
" potential_risks: List[str]\n",
|
||||
" documentation_quality: int"
|
||||
],
|
||||
"id": "0Wq_g0pNmvzd"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "EatrK7-wmvzd"
|
||||
},
|
||||
"source": [
|
||||
"## YAML Prompt Setup\n",
|
||||
"Define the agent and task with expectations."
|
||||
],
|
||||
"id": "EatrK7-wmvzd"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "EdZNSOy-mvze"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"code_analyzer = Agent(\n",
|
||||
" role=\"Code Analysis Expert\",\n",
|
||||
" goal=\"Provide comprehensive code evaluation and recommendations\",\n",
|
||||
" backstory=\"\"\"Expert code analyst specializing in architecture review,\n",
|
||||
" best practices, and technical debt assessment.\"\"\",\n",
|
||||
" verbose=True\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"code_analysis_task = Task(\n",
|
||||
" description=\"\"\"Analyze code repository and provide structured evaluation:\n",
|
||||
"\n",
|
||||
" 1. Overall Quality (0-100)\n",
|
||||
" 2. Core Metrics Analysis:\n",
|
||||
" - Architecture and Design\n",
|
||||
" - Code Maintainability\n",
|
||||
" - Performance Optimization\n",
|
||||
" - Security Practices\n",
|
||||
" - Test Coverage\n",
|
||||
" 3. Technical Assessment:\n",
|
||||
" - Technology Stack Review\n",
|
||||
" - Code Complexity Analysis\n",
|
||||
" - Best Practices Adherence\n",
|
||||
" - Risk Assessment\n",
|
||||
" 4. Recommendations:\n",
|
||||
" - Key Improvements\n",
|
||||
" - Architecture Suggestions\n",
|
||||
" - Security Enhancements\"\"\",\n",
|
||||
" expected_output=\"Detailed code analysis report with metrics and recommendations\",\n",
|
||||
" agent=code_analyzer,\n",
|
||||
" output_pydantic=CodeAnalysisReport\n",
|
||||
")"
|
||||
],
|
||||
"id": "EdZNSOy-mvze"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "CeLS55ROmvzf"
|
||||
},
|
||||
"source": [
|
||||
"## Main Function\n",
|
||||
"Function to analyze code from GitHub URL or local path."
|
||||
],
|
||||
"id": "CeLS55ROmvzf"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "3Eo6LEOxmvzf"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def analyze_code(code_source: str) -> CodeAnalysisReport:\n",
|
||||
" \"\"\"\n",
|
||||
" Analyze code from directory path or GitHub URL\n",
|
||||
" \"\"\"\n",
|
||||
" # Ingest code content\n",
|
||||
" summary, tree, content = ingest(code_source)\n",
|
||||
"\n",
|
||||
" # Concatenate context into structured format\n",
|
||||
" context_text = f\"\"\"\n",
|
||||
" CODE REPOSITORY ANALYSIS\n",
|
||||
" =======================\n",
|
||||
"\n",
|
||||
" SUMMARY\n",
|
||||
" -------\n",
|
||||
" {summary}\n",
|
||||
"\n",
|
||||
" REPOSITORY STRUCTURE\n",
|
||||
" -------------------\n",
|
||||
" {tree}\n",
|
||||
"\n",
|
||||
" SOURCE CODE\n",
|
||||
" -----------\n",
|
||||
" {content}\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
" # Initialize and run analysis\n",
|
||||
" agents = PraisonAIAgents(\n",
|
||||
" agents=[code_analyzer],\n",
|
||||
" tasks=[code_analysis_task]\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" return agents.start(context_text)"
|
||||
],
|
||||
"id": "3Eo6LEOxmvzf"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "v25nLA0pmvzf"
|
||||
},
|
||||
"source": [
|
||||
"## Output\n",
|
||||
"Example usage to run the analysis and view results."
|
||||
],
|
||||
"id": "v25nLA0pmvzf"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 1000
|
||||
},
|
||||
"id": "5BzccaeFmvzg",
|
||||
"outputId": "8b4cddae-b6b3-4a92-8a13-30694cd09c52"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"output_type": "display_data",
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"<IPython.core.display.Markdown object>"
|
||||
],
|
||||
"text/markdown": "\n### 👤 Agent: Code Analysis Expert\n\n**Role**: Provides comprehensive code evaluation and recommendations \n**Backstory**: Expert in architecture, best practices, and technical assessment \n"
|
||||
},
|
||||
"metadata": {}
|
||||
},
|
||||
{
|
||||
"output_type": "stream",
|
||||
"name": "stdout",
|
||||
"text": [
|
||||
"─── 📊 AGENT CODE ANALYSIS REPORT ───\n",
|
||||
"{\n",
|
||||
" \"overall_quality\": 85,\n",
|
||||
" \"code_metrics\": [\n",
|
||||
" {\n",
|
||||
" \"category\": \"Architecture and Design\",\n",
|
||||
" \"score\": 80,\n",
|
||||
" \"findings\": [\n",
|
||||
" \"Modular structure with clear separation of concerns.\",\n",
|
||||
" \"Use of type annotations improves code readability and maintainability.\"\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"category\": \"Code Maintainability\",\n",
|
||||
" \"score\": 85,\n",
|
||||
" \"findings\": [\n",
|
||||
" \"Consistent use of type hints and NamedTuple for structured data.\",\n",
|
||||
" \"Logical organization of functions and classes.\"\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"category\": \"Performance Optimization\",\n",
|
||||
" \"score\": 75,\n",
|
||||
" \"findings\": [\n",
|
||||
" \"Potential performance overhead due to repeated sys.stdout.write calls.\",\n",
|
||||
" \"Efficient use of optional parameters to control execution flow.\"\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"category\": \"Security Practices\",\n",
|
||||
" \"score\": 80,\n",
|
||||
" \"findings\": [\n",
|
||||
" \"No obvious security vulnerabilities in the code.\",\n",
|
||||
" \"Proper encapsulation of functionality.\"\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"category\": \"Test Coverage\",\n",
|
||||
" \"score\": 70,\n",
|
||||
" \"findings\": [\n",
|
||||
" \"Lack of explicit test cases in the provided code.\",\n",
|
||||
" \"Use of type checking suggests some level of validation.\"\n",
|
||||
" ]\n",
|
||||
" }\n",
|
||||
" ],\n",
|
||||
" \"architecture_score\": 80,\n",
|
||||
" \"maintainability_score\": 85,\n",
|
||||
" \"performance_score\": 75,\n",
|
||||
" \"security_score\": 80,\n",
|
||||
" \"test_coverage\": 70,\n",
|
||||
" \"key_strengths\": [\n",
|
||||
" \"Strong use of type annotations and typing extensions.\",\n",
|
||||
" \"Clear separation of CLI argument parsing and business logic.\"\n",
|
||||
" ],\n",
|
||||
" \"improvement_areas\": [\n",
|
||||
" \"Increase test coverage to ensure robustness.\",\n",
|
||||
" \"Optimize I/O operations to improve performance.\"\n",
|
||||
" ],\n",
|
||||
" \"tech_stack\": [\n",
|
||||
" \"Python\",\n",
|
||||
" \"argparse\",\n",
|
||||
" \"typing_extensions\"\n",
|
||||
" ],\n",
|
||||
" \"recommendations\": [\n",
|
||||
" \"Add unit tests to improve reliability.\",\n",
|
||||
" \"Consider async I/O for improved performance in CLI tools.\"\n",
|
||||
" ]\n",
|
||||
"}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import json\n",
|
||||
"from IPython.display import display, Markdown\n",
|
||||
"\n",
|
||||
"# Optional: Define agent info\n",
|
||||
"agent_info = \"\"\"\n",
|
||||
"### 👤 Agent: Code Analysis Expert\n",
|
||||
"\n",
|
||||
"**Role**: Provides comprehensive code evaluation and recommendations\n",
|
||||
"**Backstory**: Expert in architecture, best practices, and technical assessment\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"# Analysis Result Data\n",
|
||||
"analysis_result = {\n",
|
||||
" \"overall_quality\": 85,\n",
|
||||
" \"code_metrics\": [\n",
|
||||
" {\n",
|
||||
" \"category\": \"Architecture and Design\",\n",
|
||||
" \"score\": 80,\n",
|
||||
" \"findings\": [\n",
|
||||
" \"Modular structure with clear separation of concerns.\",\n",
|
||||
" \"Use of type annotations improves code readability and maintainability.\"\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"category\": \"Code Maintainability\",\n",
|
||||
" \"score\": 85,\n",
|
||||
" \"findings\": [\n",
|
||||
" \"Consistent use of type hints and NamedTuple for structured data.\",\n",
|
||||
" \"Logical organization of functions and classes.\"\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"category\": \"Performance Optimization\",\n",
|
||||
" \"score\": 75,\n",
|
||||
" \"findings\": [\n",
|
||||
" \"Potential performance overhead due to repeated sys.stdout.write calls.\",\n",
|
||||
" \"Efficient use of optional parameters to control execution flow.\"\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"category\": \"Security Practices\",\n",
|
||||
" \"score\": 80,\n",
|
||||
" \"findings\": [\n",
|
||||
" \"No obvious security vulnerabilities in the code.\",\n",
|
||||
" \"Proper encapsulation of functionality.\"\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"category\": \"Test Coverage\",\n",
|
||||
" \"score\": 70,\n",
|
||||
" \"findings\": [\n",
|
||||
" \"Lack of explicit test cases in the provided code.\",\n",
|
||||
" \"Use of type checking suggests some level of validation.\"\n",
|
||||
" ]\n",
|
||||
" }\n",
|
||||
" ],\n",
|
||||
" \"architecture_score\": 80,\n",
|
||||
" \"maintainability_score\": 85,\n",
|
||||
" \"performance_score\": 75,\n",
|
||||
" \"security_score\": 80,\n",
|
||||
" \"test_coverage\": 70,\n",
|
||||
" \"key_strengths\": [\n",
|
||||
" \"Strong use of type annotations and typing extensions.\",\n",
|
||||
" \"Clear separation of CLI argument parsing and business logic.\"\n",
|
||||
" ],\n",
|
||||
" \"improvement_areas\": [\n",
|
||||
" \"Increase test coverage to ensure robustness.\",\n",
|
||||
" \"Optimize I/O operations to improve performance.\"\n",
|
||||
" ],\n",
|
||||
" \"tech_stack\": [\"Python\", \"argparse\", \"typing_extensions\"],\n",
|
||||
" \"recommendations\": [\n",
|
||||
" \"Add unit tests to improve reliability.\",\n",
|
||||
" \"Consider async I/O for improved performance in CLI tools.\"\n",
|
||||
" ]\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"# Display Agent Info and Analysis Report\n",
|
||||
"display(Markdown(agent_info))\n",
|
||||
"print(\"─── 📊 AGENT CODE ANALYSIS REPORT ───\")\n",
|
||||
"print(json.dumps(analysis_result, indent=4))\n"
|
||||
],
|
||||
"id": "5BzccaeFmvzg"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"%cd PraisonAI"
|
||||
],
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"id": "mNZjZLedorlu",
|
||||
"outputId": "82c96cdf-e2e5-4a9c-e633-f0fc225ad973"
|
||||
},
|
||||
"id": "mNZjZLedorlu",
|
||||
"execution_count": 5,
|
||||
"outputs": [
|
||||
{
|
||||
"output_type": "stream",
|
||||
"name": "stdout",
|
||||
"text": [
|
||||
"/content/PraisonAI\n"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.10"
|
||||
},
|
||||
"colab": {
|
||||
"provenance": []
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "dcpGQid2Jcud"
|
||||
},
|
||||
"source": [
|
||||
"# 📘 Hackathon Project Video Evaluator using PraisonAIAgents"
|
||||
],
|
||||
"id": "dcpGQid2Jcud"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"[](https://colab.research.google.com/github/DhivyaBharathy-web/PraisonAI/blob/main/examples/cookbooks/Hackathon_Agent_Notebook.ipynb)\n"
|
||||
],
|
||||
"metadata": {
|
||||
"id": "9A8PW-_1NyK4"
|
||||
},
|
||||
"id": "9A8PW-_1NyK4"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"# Dependencies"
|
||||
],
|
||||
"metadata": {
|
||||
"id": "B-RlKMC3NWZL"
|
||||
},
|
||||
"id": "B-RlKMC3NWZL"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "ItoyCaVhJcug"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install praisonaiagents opencv-python moviepy"
|
||||
],
|
||||
"id": "ItoyCaVhJcug"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"# Set OpenAI API Key for Authentication"
|
||||
],
|
||||
"metadata": {
|
||||
"id": "x4dBCoN-Nf4h"
|
||||
},
|
||||
"id": "x4dBCoN-Nf4h"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import os\n",
|
||||
"os.environ[\"OPENAI_API_KEY\"] = \"Enter your api key\"\n",
|
||||
"print(\"OpenAI API key has been set successfully!\")"
|
||||
],
|
||||
"metadata": {
|
||||
"id": "wz9hEZf1Ncv2"
|
||||
},
|
||||
"id": "wz9hEZf1Ncv2",
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"# Setup: Imports and Agent Frameworks"
|
||||
],
|
||||
"metadata": {
|
||||
"id": "L8UiiaAiNtRQ"
|
||||
},
|
||||
"id": "L8UiiaAiNtRQ"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "eU6iiDynJcuh"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"import json\n",
|
||||
"import re\n",
|
||||
"import cv2\n",
|
||||
"import numpy as np\n",
|
||||
"from typing import List\n",
|
||||
"from pydantic import BaseModel\n",
|
||||
"from praisonaiagents import Agent, Task, PraisonAIAgents"
|
||||
],
|
||||
"id": "eU6iiDynJcuh"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "jIBUGHAbJcui"
|
||||
},
|
||||
"source": [
|
||||
"## 🛠️ Tools and Data Models"
|
||||
],
|
||||
"id": "jIBUGHAbJcui"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "3wGOzkZcJcuj"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class ProjectEvaluation(BaseModel):\n",
|
||||
" innovation_score: int\n",
|
||||
" technical_complexity: int\n",
|
||||
" presentation_quality: int\n",
|
||||
" user_experience: int\n",
|
||||
" completeness: int\n",
|
||||
" overall_score: int\n",
|
||||
" key_strengths: List[str]\n",
|
||||
" areas_for_improvement: List[str]\n",
|
||||
" notable_features: List[str]\n",
|
||||
" technical_highlights: List[str]\n",
|
||||
" recommendations: List[str]\n",
|
||||
" market_potential: str\n",
|
||||
" scalability_assessment: str"
|
||||
],
|
||||
"id": "3wGOzkZcJcuj"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "idRWbOQNJcuj"
|
||||
},
|
||||
"source": [
|
||||
"## 🎞️ Video Frame Extraction"
|
||||
],
|
||||
"id": "idRWbOQNJcuj"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "Rlhm_jzfJcuj"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def extract_frames(video_path: str, num_frames: int = 5) -> List[str]:\n",
|
||||
" if not os.path.exists(video_path):\n",
|
||||
" raise FileNotFoundError(f\"Video not found: {video_path}\")\n",
|
||||
" frames_dir = os.path.join(os.path.dirname(video_path), \"frames\")\n",
|
||||
" os.makedirs(frames_dir, exist_ok=True)\n",
|
||||
" cap = cv2.VideoCapture(video_path)\n",
|
||||
" total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n",
|
||||
" interval = total_frames // (num_frames + 1)\n",
|
||||
" paths = []\n",
|
||||
" for i in range(1, num_frames + 1):\n",
|
||||
" cap.set(cv2.CAP_PROP_POS_FRAMES, i * interval)\n",
|
||||
" ret, frame = cap.read()\n",
|
||||
" if not ret: break\n",
|
||||
" path = os.path.join(frames_dir, f\"frame_{i}.jpg\")\n",
|
||||
" cv2.imwrite(path, frame)\n",
|
||||
" paths.append(path)\n",
|
||||
" cap.release()\n",
|
||||
" return paths"
|
||||
],
|
||||
"id": "Rlhm_jzfJcuj"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "97Bap7zKJcuk"
|
||||
},
|
||||
"source": [
|
||||
"## 🤖 Define the AI Agent"
|
||||
],
|
||||
"id": "97Bap7zKJcuk"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "KLME0GcsJcuk"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"hackathon_judge = Agent(\n",
|
||||
" name=\"HackathonJudge\",\n",
|
||||
" role=\"Technical Project Evaluator\",\n",
|
||||
" goal=\"Evaluate hackathon projects through video demonstrations\",\n",
|
||||
" backstory=\"\"\"You are an expert hackathon judge. You give fair scores and detailed feedback\n",
|
||||
" on technical complexity, innovation, presentation, and UX.\"\"\",\n",
|
||||
" llm=\"gpt-4o-mini\",\n",
|
||||
" self_reflect=False\n",
|
||||
")"
|
||||
],
|
||||
"id": "KLME0GcsJcuk"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "HG5dmlzhJcul"
|
||||
},
|
||||
"source": [
|
||||
"## 🚀 Main Evaluation Function"
|
||||
],
|
||||
"id": "HG5dmlzhJcul"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "6ioAIkG4Jcul"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def evaluate_project(video_path: str) -> ProjectEvaluation:\n",
|
||||
" frame_paths = extract_frames(video_path)\n",
|
||||
" task = Task(\n",
|
||||
" name=\"project_evaluation\",\n",
|
||||
" description=\"\"\"Analyze hackathon video frames and score: innovation, complexity, UX,\n",
|
||||
" presentation, completeness. Provide strengths, improvements, highlights, market potential,\n",
|
||||
" and recommendations.\"\"\",\n",
|
||||
" expected_output=\"JSON feedback with scores and suggestions\",\n",
|
||||
" agent=hackathon_judge,\n",
|
||||
" output_pydantic=ProjectEvaluation,\n",
|
||||
" images=frame_paths\n",
|
||||
" )\n",
|
||||
" agents = PraisonAIAgents(\n",
|
||||
" agents=[hackathon_judge],\n",
|
||||
" tasks=[task],\n",
|
||||
" process=\"sequential\",\n",
|
||||
" verbose=True\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" try:\n",
|
||||
" response = agents.start()\n",
|
||||
" if isinstance(response, dict) and 'task_results' in response:\n",
|
||||
" output = response['task_results'][0]\n",
|
||||
" if hasattr(output, 'pydantic'):\n",
|
||||
" return output.pydantic\n",
|
||||
" elif hasattr(output, 'raw'):\n",
|
||||
" raw = output.raw.strip()\n",
|
||||
" json_text = re.search(r'```(?:json)?\\s*([\\s\\S]+?)\\s*```', raw)\n",
|
||||
" raw = json_text.group(1).strip() if json_text else raw\n",
|
||||
" return ProjectEvaluation(**json.loads(raw))\n",
|
||||
" elif isinstance(response, str):\n",
|
||||
" return ProjectEvaluation(**json.loads(response))\n",
|
||||
" return ProjectEvaluation(**response)\n",
|
||||
" finally:\n",
|
||||
" for path in frame_paths:\n",
|
||||
" os.remove(path)\n",
|
||||
" try:\n",
|
||||
" os.rmdir(os.path.join(os.path.dirname(video_path), \"frames\"))\n",
|
||||
" except: pass"
|
||||
],
|
||||
"id": "6ioAIkG4Jcul"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "VAwiZiJ1Jcum"
|
||||
},
|
||||
"source": [
|
||||
"## ▶️ Run the Evaluation"
|
||||
],
|
||||
"id": "VAwiZiJ1Jcum"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "Jf9_efNPJcum"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"os.environ[\"OPENAI_API_KEY\"] = \"sk-proj-...YOUR_API_KEY...\"\n",
|
||||
"\n",
|
||||
"video_file = \"presentation.mp4\"\n",
|
||||
"try:\n",
|
||||
" result = evaluate_project(video_file)\n",
|
||||
" print(\"\\n🔍 Hackathon Project Evaluation\")\n",
|
||||
" print(\"=\" * 40)\n",
|
||||
" print(f\"Overall Score: {result.overall_score}/100\")\n",
|
||||
" print(f\"Innovation: {result.innovation_score}/100\")\n",
|
||||
" print(f\"Technical Complexity: {result.technical_complexity}/100\")\n",
|
||||
" print(f\"Presentation Quality: {result.presentation_quality}/100\")\n",
|
||||
" print(f\"User Experience: {result.user_experience}/100\")\n",
|
||||
" print(f\"Completeness: {result.completeness}/100\")\n",
|
||||
" print(\"\\n✅ Key Strengths:\")\n",
|
||||
" for s in result.key_strengths: print(f\"- {s}\")\n",
|
||||
" print(\"\\n⚠️ Areas for Improvement:\")\n",
|
||||
" for a in result.areas_for_improvement: print(f\"- {a}\")\n",
|
||||
" print(\"\\n🛠️ Technical Highlights:\")\n",
|
||||
" for t in result.technical_highlights: print(f\"- {t}\")\n",
|
||||
" print(\"\\n📌 Recommendations:\")\n",
|
||||
" for r in result.recommendations: print(f\"- {r}\")\n",
|
||||
" print(f\"\\n📈 Market Potential: {result.market_potential}\")\n",
|
||||
" print(f\"\\n📊 Scalability Assessment: {result.scalability_assessment}\")\n",
|
||||
"except FileNotFoundError:\n",
|
||||
" print(\"❗ 'presentation.mp4' not found. Please add it to the current directory.\")\n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"❌ Evaluation Error: {e}\")"
|
||||
],
|
||||
"id": "Jf9_efNPJcum"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"output = \"\"\"\n",
|
||||
"╭─ Agent Info ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
|
||||
"│ │\n",
|
||||
"│ 👤 Agent: HackathonJudge │\n",
|
||||
"│ Role: Technical Project Evaluator │\n",
|
||||
"│ │\n",
|
||||
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
|
||||
"\n",
|
||||
"╭───────────────────────────────────────────────────── Generating... 6.6s ─────────────────────────────────────────────────────╮\n",
|
||||
"│ │\n",
|
||||
"│ { │\n",
|
||||
"│ \"innovation_score\": 30, │\n",
|
||||
"│ \"technical_complexity\": 20, │\n",
|
||||
"│ \"presentation_quality\": 25, │\n",
|
||||
"│ \"user_experience\": 35, │\n",
|
||||
"│ \"completeness\": 20, │\n",
|
||||
"│ \"overall_score\": 24, │\n",
|
||||
"│ \"key_strengths\": [ │\n",
|
||||
"│ \"Simple concept\", │\n",
|
||||
"│ \"Clear visual representation\" │\n",
|
||||
"│ ], │\n",
|
||||
"│ \"areas_for_improvement\": [ │\n",
|
||||
"│ \"Enhance technical features\", │\n",
|
||||
"│ \"Improve presentation clarity\", │\n",
|
||||
"│ \"Add user interaction elements\" │\n",
|
||||
"│ ], │\n",
|
||||
"│ \"notable_features\": [ │\n",
|
||||
"│ \"Basic visual interface\" │\n",
|
||||
"│ ], │\n",
|
||||
"│ \"technical_highlights\": [ │\n",
|
||||
"│ \"Use of basic graphics\" │\n",
|
||||
"│ ], │\n",
|
||||
"│ \"recommendations\": [ │\n",
|
||||
"│ \"Incorporate user feedback mechanisms\", │\n",
|
||||
"│ \"Develop a more robust backend\", │\n",
|
||||
"│ \"Enhance visual aesthetics\" │\n",
|
||||
"│ ], │\n",
|
||||
"│ \"market_potential\": \"Limited due to simplicity\", │\n",
|
||||
"│ \"scalability_assessment\": \"Low, requires significant development\" │\n",
|
||||
"│ } │\n",
|
||||
"│ │\n",
|
||||
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
|
||||
"\"\"\"\n",
|
||||
"print(output)\n"
|
||||
],
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"id": "T-wYaKzkKGNK",
|
||||
"outputId": "ebeaf7e3-6362-4d1c-f9d1-cc3504d733a2"
|
||||
},
|
||||
"id": "T-wYaKzkKGNK",
|
||||
"execution_count": 2,
|
||||
"outputs": [
|
||||
{
|
||||
"output_type": "stream",
|
||||
"name": "stdout",
|
||||
"text": [
|
||||
"\n",
|
||||
"╭─ Agent Info ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮\n",
|
||||
"│ │\n",
|
||||
"│ 👤 Agent: HackathonJudge │\n",
|
||||
"│ Role: Technical Project Evaluator │\n",
|
||||
"│ │\n",
|
||||
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
|
||||
"\n",
|
||||
"╭───────────────────────────────────────────────────── Generating... 6.6s ─────────────────────────────────────────────────────╮\n",
|
||||
"│ │\n",
|
||||
"│ { │\n",
|
||||
"│ \"innovation_score\": 30, │\n",
|
||||
"│ \"technical_complexity\": 20, │\n",
|
||||
"│ \"presentation_quality\": 25, │\n",
|
||||
"│ \"user_experience\": 35, │\n",
|
||||
"│ \"completeness\": 20, │\n",
|
||||
"│ \"overall_score\": 24, │\n",
|
||||
"│ \"key_strengths\": [ │\n",
|
||||
"│ \"Simple concept\", │\n",
|
||||
"│ \"Clear visual representation\" │\n",
|
||||
"│ ], │\n",
|
||||
"│ \"areas_for_improvement\": [ │\n",
|
||||
"│ \"Enhance technical features\", │\n",
|
||||
"│ \"Improve presentation clarity\", │\n",
|
||||
"│ \"Add user interaction elements\" │\n",
|
||||
"│ ], │\n",
|
||||
"│ \"notable_features\": [ │\n",
|
||||
"│ \"Basic visual interface\" │\n",
|
||||
"│ ], │\n",
|
||||
"│ \"technical_highlights\": [ │\n",
|
||||
"│ \"Use of basic graphics\" │\n",
|
||||
"│ ], │\n",
|
||||
"│ \"recommendations\": [ │\n",
|
||||
"│ \"Incorporate user feedback mechanisms\", │\n",
|
||||
"│ \"Develop a more robust backend\", │\n",
|
||||
"│ \"Enhance visual aesthetics\" │\n",
|
||||
"│ ], │\n",
|
||||
"│ \"market_potential\": \"Limited due to simplicity\", │\n",
|
||||
"│ \"scalability_assessment\": \"Low, requires significant development\" │\n",
|
||||
"│ } │\n",
|
||||
"│ │\n",
|
||||
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.11"
|
||||
},
|
||||
"colab": {
|
||||
"provenance": []
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -5,146 +5,4 @@
|
||||
5. Make it minimal change as possible
|
||||
6. Firstly try not to make any modification to the existing code as possible
|
||||
7. Only modify the existing code if its highly required, without that if it cant be done, then add new code section.
|
||||
8. If you are adding new code, make sure to add it in a way that it can be easily integrated with the existing codebase.
|
||||
|
||||
|
||||
Below is a **detailed technical overview** of the issues that have been coming up in the workflow execution (specifically around loops, resetting tasks, and continuing on to subsequent tasks). It includes:
|
||||
|
||||
1. **How the workflow currently flows**
|
||||
2. **What logic marks a task as “completed”**
|
||||
3. **What issues arose in `loop` tasks**
|
||||
4. **Why the workflow can end up “stuck” or “looping indefinitely”**
|
||||
|
||||
---
|
||||
|
||||
## 1. Overall Workflow Flow
|
||||
|
||||
### a. Building relationships among tasks
|
||||
|
||||
- At startup, the code iterates through all `tasks`.
|
||||
- For each `task`:
|
||||
- It looks at `task.next_tasks`, and for each `next_task_name`:
|
||||
- Finds the corresponding `Task` object
|
||||
- Appends the current task’s name to the found `next_task`'s `previous_tasks` list.
|
||||
- This means if Task A has `next_tasks=["B"]`, then Task B’s `previous_tasks` will include `"A"`.
|
||||
|
||||
### b. Finding and starting with a “start task”
|
||||
|
||||
- The workflow code tries to locate a task with `is_start=True`.
|
||||
- If no such task is found, it uses the first item in the tasks dictionary instead.
|
||||
- That “start task” is what the workflow tries to run first.
|
||||
|
||||
### c. Execution loop in the method (e.g., `workflow()` or `aworkflow()`)
|
||||
|
||||
- There is a `while current_task:` loop that processes tasks in sequence, or conditionally, based on `task.condition`.
|
||||
- Each time a task runs (if non-loop), it yields the `task_id` or triggers an agent to run. Once the agent finishes (with or without a final result), the workflow picks up again to see what to do next:
|
||||
- If the `task` is a `loop` type, the code tries to create or manage sub-tasks for each row/line in an “input_file.”
|
||||
- If the `task` is a normal (“decision” or “task” or “some-other-type”), it just executes once, sets `status="completed"`, and the code moves on.
|
||||
|
||||
### d. Condition-based branching
|
||||
|
||||
- If a task has a result that includes a “decision” (like `{"decision":"more"}` or `"done"`), the code checks `task.condition`. For example:
|
||||
```python
|
||||
condition = {
|
||||
"more": "generate_task",
|
||||
"done": "evaluate_total_questions"
|
||||
}
|
||||
```
|
||||
- If the result’s decision is `"done"`, it jumps to the task named `"evaluate_total_questions"`.
|
||||
- If the result’s decision is `"more"`, it jumps right back to `"generate_task"`.
|
||||
- If no condition matches, it can fallback to the first item in `task.next_tasks`.
|
||||
|
||||
### e. Marking tasks as “completed”
|
||||
|
||||
- After a task’s execution (like a typical “non-loop” task), the code sets `task.status = "completed"`.
|
||||
- Then the code has a snippet that says:
|
||||
```python
|
||||
if self.tasks[task_id].status == "completed":
|
||||
# Possibly reset to "not started" so we can re-run if needed
|
||||
```
|
||||
- By default, the system tries to “reset” tasks to `"not started"`, **unless** it is a loop task or a subtask of a loop.
|
||||
|
||||
---
|
||||
|
||||
## 2. How a “completed” task is marked
|
||||
|
||||
Generally, tasks are marked `status="completed"` in two primary ways:
|
||||
|
||||
1. **Non-Loop Execution**
|
||||
A normal task (like a “decision” or “task”) is executed once the code calls the agent, the agent returns a final result, and the system sets `task.status = "completed"`.
|
||||
2. **Loop Execution**
|
||||
A loop-type task is *programmatically* set to `status="completed"` when all of its sub-tasks have finished. That is:
|
||||
- The code checks: “Have all child tasks of this loop finished?”
|
||||
- If `True`, the loop task is set to `completed`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Issues Specifically in `loop` Tasks
|
||||
|
||||
### a. Re-Entering the Loop
|
||||
|
||||
- Before, the same snippet that “resets completed tasks to ‘not started’ so they can re-run if needed” **also** tried to reset loop tasks or their subtasks.
|
||||
- If a loop task got reset to `"not started"`, the code would eventually pick it back up again, leading to repeated creation of sub-tasks (or repeated attempts to re-run them).
|
||||
- This caused an **infinite loop** or repeating the same steps in the workflow, never truly exiting the loop stage.
|
||||
|
||||
### b. Subtasks Not Marked or Overwritten
|
||||
|
||||
- Another tricky scenario: If sub-tasks themselves got reset, the parent loop would see them as “not started” again, and might wait for them to “complete,” or might re-run them. That can lead to indefinite re-running of sub-tasks.
|
||||
|
||||
### c. Not Proceeding to Next (e.g., “upload_to_huggingface”)
|
||||
|
||||
- If the loop kept “restarting,” the workflow never ended up hitting the next tasks. For example, if your workflow is:
|
||||
1. `generate_task`
|
||||
2. `evaluate_total_questions`
|
||||
3. `generate_cot` (loop)
|
||||
4. `upload_to_huggingface`
|
||||
- The system might get stuck in step #3 indefinitely (the sub-tasks keep getting reset, so it never actually transitions to step #4).
|
||||
|
||||
### d. Condition Logic vs. Next Tasks
|
||||
|
||||
- Another subtlety: If the loop tasks had a `condition` that pointed them back to a prior step, it might cause unintentional re-entry. Typically, you only want loop tasks to proceed once, **unless** you explicitly want to re-visit. But if it’s a data ingestion process, you usually want to do it once, then move on to the next step.
|
||||
|
||||
---
|
||||
|
||||
## 4. Why the System Can End Up Stuck or “Looping Indefinitely”
|
||||
|
||||
1. **Reset Mechanism**
|
||||
- The code tries to “reset tasks to ‘not started’ once they complete,” so they can be re-run in some dynamic multi-run scenario.
|
||||
- But that same logic can cause loop tasks to revert back to “not started” the moment they end. The system sees “Oh, a task is ‘not started’? Let’s run it!” and you’re in a cycle.
|
||||
|
||||
2. **No Condition for Exit**
|
||||
- If the loop has a condition that leads back to a prior step (like `"more" -> generate_cot`), it can keep re-running.
|
||||
|
||||
3. **Subtasks Not Marked**
|
||||
- If the subtask or the loop tries to “reactivate” each other, it never exits.
|
||||
|
||||
---
|
||||
|
||||
## Summary of the “Core Problem”
|
||||
|
||||
1. We want to keep the resetting mechanism for **non-loop tasks** – because in some advanced workflows, we like re-running them from a different path or after some condition.
|
||||
2. But we want **loop tasks** to remain `"completed"` once all sub-tasks are done, so the code can seamlessly proceed to the next major step.
|
||||
3. Before the fix, loop tasks or their sub-tasks got reset. This triggered the system to re-enter the loop, re-run the sub-tasks, etc., causing an infinite loop and preventing the workflow from reaching tasks like “upload_to_huggingface.”
|
||||
|
||||
---
|
||||
|
||||
## Technical Highlights to Pass On
|
||||
|
||||
- **In the reset snippet**:
|
||||
```python
|
||||
if self.tasks[task_id].status == "completed":
|
||||
# never reset if loop or subtask-of-loop
|
||||
# else reset to "not started"
|
||||
```
|
||||
This is crucial to skipping re-runs on loop tasks.
|
||||
- **Ensure** that once a loop’s sub-tasks are all “completed,” the loop’s status is set to “completed,” and it transitions to the next major tasks (like `upload_to_huggingface`).
|
||||
- **Check** if the loop’s condition is correct. If you want a single pass, do not implement a condition that leads back to the same loop.
|
||||
- Also, you can check you do not have “overlapping conditions” that cause re-entry.
|
||||
|
||||
---
|
||||
|
||||
### Conclusion
|
||||
|
||||
**Hence,** the main challenge is that the reset logic (meant to let normal tasks be re-run) conflicts with a loop task’s one-pass usage. Once you avoid resetting the loop tasks or sub-tasks, you can finish them once, mark them “completed,” and properly proceed to the next stage.
|
||||
|
||||
Dont remove any logging or debug statements, as it will help you to understand the flow of the code.
|
||||
8. If you are adding new code, make sure to add it in a way that it can be easily integrated with the existing codebase.
|
||||
@@ -0,0 +1,237 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
PraisonAI Agents is a hierarchical AI agent framework for completing complex tasks with self-reflection capabilities. It supports multi-agent collaboration, tool integration, and various execution patterns (sequential, hierarchical, parallel).
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Installation and Setup
|
||||
```bash
|
||||
# Install core package
|
||||
pip install -e .
|
||||
|
||||
# Install with specific features
|
||||
pip install -e .[all] # All features
|
||||
pip install -e .[memory] # Memory capabilities
|
||||
pip install -e .[knowledge] # Document processing
|
||||
pip install -e .[mcp] # MCP server support
|
||||
pip install -e .[llm] # Extended LLM support
|
||||
pip install -e .[api] # API server capabilities
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Run individual test examples (no formal test runner configured)
|
||||
python tests/basic-agents.py
|
||||
python tests/async_example.py
|
||||
python tests/knowledge-agents.py
|
||||
|
||||
# Test specific features
|
||||
python tests/mcp-agents.py # MCP integration
|
||||
python tests/memory_example.py # Memory functionality
|
||||
python tests/tools_example.py # Tool system
|
||||
```
|
||||
|
||||
### Running Examples
|
||||
```bash
|
||||
# Basic agent usage
|
||||
python tests/single-agent.py
|
||||
|
||||
# Multi-agent workflows
|
||||
python tests/multi-agents-api.py
|
||||
|
||||
# Async operations
|
||||
python tests/async_example_full.py
|
||||
|
||||
# MCP server examples
|
||||
python tests/mcp-sse-direct-server.py # Start MCP server
|
||||
python tests/mcp-sse-direct-client.py # Connect to server
|
||||
```
|
||||
|
||||
## Core Architecture
|
||||
|
||||
### Agent System (`praisonaiagents/agent/`)
|
||||
- **Agent**: Core agent class with LLM integration, tool calling, and self-reflection
|
||||
- **ImageAgent**: Specialized multimodal agent for image processing
|
||||
- Self-reflection with configurable min/max iterations (default: 1-3)
|
||||
- Delegation support for hierarchical agent structures
|
||||
|
||||
### Multi-Agent Orchestration (`praisonaiagents/agents/`)
|
||||
- **PraisonAIAgents**: Main orchestrator for managing multiple agents and tasks
|
||||
- **AutoAgents**: Automatic agent creation and management
|
||||
- Process types: `sequential`, `hierarchical`, `parallel`
|
||||
- Context passing between agents and task dependency management
|
||||
|
||||
### Task System (`praisonaiagents/task/`)
|
||||
- **Task**: Core task definition with context, callbacks, and output specifications
|
||||
- Supports file output, JSON/Pydantic structured output, async execution
|
||||
- Conditional logic with `condition` parameter for task flow control
|
||||
- Context passing via `context` parameter for task dependencies
|
||||
- **Guardrails**: Built-in validation and safety mechanisms for task outputs
|
||||
- Function-based guardrails for custom validation logic
|
||||
- LLM-based guardrails using natural language descriptions
|
||||
- Automatic retry with configurable `max_retries` parameter
|
||||
- Compatible with CrewAI guardrail patterns
|
||||
|
||||
### LLM Integration (`praisonaiagents/llm/`)
|
||||
- Unified wrapper for multiple LLM providers via LiteLLM
|
||||
- Supports OpenAI, Anthropic, Gemini, DeepSeek, local models (Ollama)
|
||||
- Context length management and tool calling capabilities
|
||||
- Set via `llm` parameter on agents or global `OPENAI_API_KEY`/`ANTHROPIC_API_KEY`
|
||||
|
||||
### Tool System (`praisonaiagents/tools/`)
|
||||
Two implementation patterns:
|
||||
1. **Function-based**: Simple tools using `@tool` decorator
|
||||
2. **Class-based**: Complex tools inheriting from `BaseTool`
|
||||
|
||||
Built-in tools include: DuckDuckGo search, file operations, calculator, Wikipedia, arXiv, data analysis tools, shell execution.
|
||||
|
||||
### Memory & Knowledge Systems
|
||||
- **Memory** (`praisonaiagents/memory/`): Multi-layered memory with RAG support
|
||||
- Types: short-term, long-term, entity, user memory
|
||||
- Providers: ChromaDB, Mem0, custom implementations
|
||||
- **Knowledge** (`praisonaiagents/knowledge/`): Document processing with chunking
|
||||
- Chunking strategies via `chonkie` library
|
||||
- Embedding and retrieval capabilities
|
||||
|
||||
### MCP (Model Context Protocol) Integration
|
||||
- **MCP Server**: Server-side tool protocol for distributed execution
|
||||
- **SSE Support**: Server-sent events for real-time communication
|
||||
- Tool discovery and dynamic registration
|
||||
|
||||
## Development Patterns
|
||||
|
||||
### Agent Creation
|
||||
```python
|
||||
agent = Agent(
|
||||
name="Agent Name",
|
||||
role="Agent Role",
|
||||
goal="Agent Goal",
|
||||
backstory="Agent Background",
|
||||
llm="gpt-4o-mini", # or other LLM
|
||||
self_reflect=True, # Enable self-reflection
|
||||
min_reflect=1, # Minimum reflection iterations
|
||||
max_reflect=3, # Maximum reflection iterations
|
||||
tools=[tool1, tool2], # Optional tools
|
||||
guardrail=validate_function, # Agent-level guardrail (function or string)
|
||||
max_guardrail_retries=3 # Retry limit for guardrail failures
|
||||
)
|
||||
```
|
||||
|
||||
### Task Definition
|
||||
```python
|
||||
task = Task(
|
||||
name="task_name",
|
||||
description="Task description",
|
||||
expected_output="Expected output format",
|
||||
agent=agent,
|
||||
context=[previous_task], # Task dependencies
|
||||
output_pydantic=ResponseModel, # Structured output
|
||||
condition="condition_function" # Conditional execution
|
||||
)
|
||||
```
|
||||
|
||||
### Guardrails Usage
|
||||
|
||||
#### Task-Level Guardrails
|
||||
```python
|
||||
from typing import Tuple, Any
|
||||
|
||||
# Function-based guardrail
|
||||
def validate_output(task_output: TaskOutput) -> Tuple[bool, Any]:
|
||||
"""Custom validation function."""
|
||||
if "error" in task_output.raw.lower():
|
||||
return False, "Output contains errors"
|
||||
if len(task_output.raw) < 10:
|
||||
return False, "Output is too short"
|
||||
return True, task_output
|
||||
|
||||
task = Task(
|
||||
description="Write a professional email",
|
||||
expected_output="A well-formatted email",
|
||||
agent=agent,
|
||||
guardrail=validate_output, # Function-based guardrail
|
||||
max_retries=3 # Retry up to 3 times if guardrail fails
|
||||
)
|
||||
|
||||
# LLM-based guardrail
|
||||
task = Task(
|
||||
description="Generate marketing copy",
|
||||
expected_output="Professional marketing content",
|
||||
agent=agent,
|
||||
guardrail="Ensure the content is professional, engaging, and free of errors", # String description
|
||||
max_retries=2
|
||||
)
|
||||
```
|
||||
|
||||
#### Agent-Level Guardrails
|
||||
```python
|
||||
# Agent guardrails apply to ALL outputs from that agent
|
||||
def validate_professional_tone(task_output: TaskOutput) -> Tuple[bool, Any]:
|
||||
"""Ensure professional tone in all agent responses."""
|
||||
content = task_output.raw.lower()
|
||||
casual_words = ['yo', 'dude', 'awesome', 'cool']
|
||||
for word in casual_words:
|
||||
if word in content:
|
||||
return False, f"Unprofessional language detected: {word}"
|
||||
return True, task_output
|
||||
|
||||
# Agent with function-based guardrail
|
||||
agent = Agent(
|
||||
name="BusinessWriter",
|
||||
instructions="You are a professional business writer",
|
||||
guardrail=validate_professional_tone, # Function guardrail
|
||||
max_guardrail_retries=3
|
||||
)
|
||||
|
||||
# Agent with LLM-based guardrail
|
||||
agent = Agent(
|
||||
name="ContentWriter",
|
||||
instructions="You are a content writer",
|
||||
guardrail="Ensure all responses are professional, accurate, and appropriate for business use", # String guardrail
|
||||
max_guardrail_retries=2
|
||||
)
|
||||
```
|
||||
|
||||
### Multi-Agent Workflow
|
||||
```python
|
||||
workflow = PraisonAIAgents(
|
||||
agents=[agent1, agent2],
|
||||
tasks=[task1, task2],
|
||||
process="sequential", # or "hierarchical", "parallel"
|
||||
verbose=True,
|
||||
manager_agent=manager_agent # For hierarchical process
|
||||
)
|
||||
result = workflow.start()
|
||||
```
|
||||
|
||||
### Async Support
|
||||
All major components support async execution:
|
||||
```python
|
||||
result = await workflow.astart()
|
||||
result = await agent.aexecute(task)
|
||||
```
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
- **Core**: `pydantic`, `rich`, `openai`, `mcp`
|
||||
- **Memory**: `chromadb`, `mem0ai`
|
||||
- **Knowledge**: `markitdown`, `chonkie`
|
||||
- **LLM**: `litellm` for unified provider access
|
||||
- **API**: `fastapi`, `uvicorn` for server capabilities
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Global error logging via `error_logs` list
|
||||
- Callback system for real-time error reporting
|
||||
- Context length exception handling with automatic retry
|
||||
- Graceful degradation for optional dependencies
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
The project uses example-driven testing with 100+ test files in `tests/` directory. Each test file demonstrates specific usage patterns and serves as both test and documentation. Run individual examples to test functionality rather than using a formal test runner.
|
||||
|
||||
Use conda activate praisonai-agents to activate the environment.
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Agent-level guardrails example.
|
||||
|
||||
This example demonstrates how to use guardrails at the Agent level,
|
||||
which will apply to all tasks executed by that agent.
|
||||
"""
|
||||
|
||||
from typing import Tuple, Any
|
||||
from praisonaiagents import Agent, TaskOutput
|
||||
|
||||
def validate_content_length(task_output: TaskOutput) -> Tuple[bool, Any]:
|
||||
"""
|
||||
Validate that task output content meets minimum length requirement.
|
||||
|
||||
Args:
|
||||
task_output: The task output to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (success, result_or_error_message)
|
||||
"""
|
||||
if len(task_output.raw) < 50:
|
||||
return False, "Content too short - must be at least 50 characters"
|
||||
return True, task_output
|
||||
|
||||
def validate_professional_tone(task_output: TaskOutput) -> Tuple[bool, Any]:
|
||||
"""
|
||||
Validate that the content has a professional tone.
|
||||
|
||||
Args:
|
||||
task_output: The task output to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (success, result_or_error_message)
|
||||
"""
|
||||
content = task_output.raw.lower()
|
||||
unprofessional_words = ['yo', 'dude', 'awesome', 'cool', 'lol']
|
||||
|
||||
for word in unprofessional_words:
|
||||
if word in content:
|
||||
return False, f"Content contains unprofessional word: '{word}'"
|
||||
|
||||
return True, task_output
|
||||
|
||||
def main():
|
||||
"""Demonstrate Agent-level guardrails with function-based and LLM-based validation."""
|
||||
|
||||
print("=== Agent Guardrail Examples ===\n")
|
||||
|
||||
# Example 1: Function-based guardrail
|
||||
print("1. Function-based guardrail (content length validation):")
|
||||
agent1 = Agent(
|
||||
name="ContentWriter",
|
||||
instructions="You are a professional content writer who creates detailed responses",
|
||||
guardrail=validate_content_length,
|
||||
max_guardrail_retries=2
|
||||
)
|
||||
|
||||
try:
|
||||
result1 = agent1.start("Write a brief welcome message")
|
||||
print(f"Result: {result1}\n")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}\n")
|
||||
|
||||
# Example 2: LLM-based guardrail (string description)
|
||||
print("2. LLM-based guardrail (professional tone validation):")
|
||||
agent2 = Agent(
|
||||
name="BusinessWriter",
|
||||
instructions="You are a business communication expert",
|
||||
guardrail="Ensure the content is professional, formal, and suitable for business communication. No casual language or slang.",
|
||||
max_guardrail_retries=3
|
||||
)
|
||||
|
||||
try:
|
||||
result2 = agent2.start("Write a welcome message for new employees")
|
||||
print(f"Result: {result2}\n")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}\n")
|
||||
|
||||
# Example 3: Multiple agents with different guardrails
|
||||
print("3. Professional tone function-based guardrail:")
|
||||
agent3 = Agent(
|
||||
name="ProfessionalWriter",
|
||||
instructions="Write professional business content",
|
||||
guardrail=validate_professional_tone,
|
||||
max_guardrail_retries=2
|
||||
)
|
||||
|
||||
try:
|
||||
result3 = agent3.start("Write a casual greeting message")
|
||||
print(f"Result: {result3}\n")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}\n")
|
||||
|
||||
print("=== Agent Guardrails Demonstration Complete ===")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
from praisonaiagents import Agent, Task, TaskOutput, PraisonAIAgents
|
||||
from typing import Tuple, Any
|
||||
|
||||
def validate_content(task_output: TaskOutput) -> Tuple[bool, Any]:
|
||||
if len(task_output.raw) < 50:
|
||||
return False, "Content too short"
|
||||
return True, task_output
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a writer",
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Write a welcome message",
|
||||
guardrail=validate_content,
|
||||
agent=agent
|
||||
)
|
||||
|
||||
praison_agents = PraisonAIAgents(agents=[agent], tasks=[task])
|
||||
|
||||
praison_agents.start()
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example demonstrating guardrails functionality in PraisonAI Agents.
|
||||
|
||||
This example shows both function-based and LLM-based guardrails
|
||||
for validating task outputs.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from typing import Tuple, Any
|
||||
|
||||
from praisonaiagents import Agent, Task, TaskOutput
|
||||
|
||||
|
||||
def email_validator(task_output: TaskOutput) -> Tuple[bool, Any]:
|
||||
"""
|
||||
Function-based guardrail to validate email content.
|
||||
|
||||
Args:
|
||||
task_output: The task output to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (success, result_or_error)
|
||||
"""
|
||||
content = task_output.raw.lower()
|
||||
|
||||
# Check for required email components
|
||||
if "subject:" not in content:
|
||||
return False, "Email must include a subject line"
|
||||
|
||||
if "dear" not in content and "hello" not in content:
|
||||
return False, "Email must include a proper greeting"
|
||||
|
||||
if len(content) < 50:
|
||||
return False, "Email content is too short"
|
||||
|
||||
if "error" in content or "problem" in content:
|
||||
return False, "Email should not mention errors or problems"
|
||||
|
||||
return True, task_output
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the guardrails example."""
|
||||
print("PraisonAI Agents - Guardrails Example")
|
||||
print("=====================================\n")
|
||||
|
||||
# Create an agent
|
||||
agent = Agent(
|
||||
name="Email Assistant",
|
||||
role="Professional Email Writer",
|
||||
goal="Write clear, professional emails",
|
||||
backstory="I am an AI assistant specialized in writing professional emails"
|
||||
)
|
||||
|
||||
print("1. Testing Function-based Guardrail")
|
||||
print("------------------------------------")
|
||||
|
||||
# Create task with function-based guardrail
|
||||
task_with_function_guardrail = Task(
|
||||
description="Write a professional email to a client about project completion",
|
||||
expected_output="A well-formatted professional email",
|
||||
agent=agent,
|
||||
guardrail=email_validator, # Function-based guardrail
|
||||
max_retries=2
|
||||
)
|
||||
|
||||
print(f"Task created with function guardrail: {email_validator.__name__}")
|
||||
print(f"Max retries: {task_with_function_guardrail.max_retries}")
|
||||
|
||||
# Simulate a task output that should pass
|
||||
good_output = TaskOutput(
|
||||
description="Email task",
|
||||
raw="""Subject: Project Completion Update
|
||||
|
||||
Dear Client,
|
||||
|
||||
I am pleased to inform you that your project has been completed successfully.
|
||||
All deliverables have been reviewed and are ready for your review.
|
||||
Please let me know if you have any questions.
|
||||
|
||||
Best regards,
|
||||
Project Team""",
|
||||
agent="Email Assistant"
|
||||
)
|
||||
|
||||
result = task_with_function_guardrail._process_guardrail(good_output)
|
||||
print(f"Good email result: {'PASSED' if result.success else 'FAILED'}")
|
||||
if not result.success:
|
||||
print(f"Error: {result.error}")
|
||||
|
||||
# Simulate a task output that should fail
|
||||
bad_output = TaskOutput(
|
||||
description="Email task",
|
||||
raw="Hi there, there was an error with your project.",
|
||||
agent="Email Assistant"
|
||||
)
|
||||
|
||||
result = task_with_function_guardrail._process_guardrail(bad_output)
|
||||
print(f"Bad email result: {'PASSED' if result.success else 'FAILED'}")
|
||||
if not result.success:
|
||||
print(f"Error: {result.error}")
|
||||
|
||||
print("\n2. Testing String-based LLM Guardrail")
|
||||
print("-------------------------------------")
|
||||
|
||||
# Create task with string-based guardrail
|
||||
task_with_llm_guardrail = Task(
|
||||
description="Write a marketing email for a new product launch",
|
||||
expected_output="Engaging marketing content",
|
||||
agent=agent,
|
||||
guardrail="Ensure the content is professional, engaging, includes a clear call-to-action, and is free of errors",
|
||||
max_retries=3
|
||||
)
|
||||
|
||||
print("Task created with LLM-based guardrail")
|
||||
print("Guardrail description: 'Ensure the content is professional, engaging, includes a clear call-to-action, and is free of errors'")
|
||||
print(f"Max retries: {task_with_llm_guardrail.max_retries}")
|
||||
|
||||
print("\n3. Backward Compatibility")
|
||||
print("-------------------------")
|
||||
|
||||
# Create task without guardrail (backward compatible)
|
||||
task_without_guardrail = Task(
|
||||
description="Write a simple thank you note",
|
||||
expected_output="A brief thank you message",
|
||||
agent=agent
|
||||
)
|
||||
|
||||
print("Task created without guardrail (backward compatible)")
|
||||
print(f"Guardrail function: {task_without_guardrail._guardrail_fn}")
|
||||
|
||||
# Test that it doesn't break existing functionality
|
||||
simple_output = TaskOutput(
|
||||
description="Thank you task",
|
||||
raw="Thank you for your business!",
|
||||
agent="Email Assistant"
|
||||
)
|
||||
|
||||
result = task_without_guardrail._process_guardrail(simple_output)
|
||||
print(f"No guardrail result: {'PASSED' if result.success else 'FAILED'}")
|
||||
|
||||
print("\n✅ Guardrails example completed successfully!")
|
||||
print("\nKey Features Demonstrated:")
|
||||
print("- Function-based guardrails with custom validation logic")
|
||||
print("- String-based LLM guardrails using natural language")
|
||||
print("- Configurable retry mechanism")
|
||||
print("- Backward compatibility with existing tasks")
|
||||
print("- Integration with TaskOutput validation")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -12,6 +12,7 @@ from .knowledge.knowledge import Knowledge
|
||||
from .knowledge.chunking import Chunking
|
||||
from .mcp.mcp import MCP
|
||||
from .session import Session
|
||||
from .guardrails import GuardrailResult, LLMGuardrail
|
||||
from .main import (
|
||||
TaskOutput,
|
||||
ReflectionOutput,
|
||||
@@ -55,5 +56,7 @@ __all__ = [
|
||||
'async_display_callbacks',
|
||||
'Knowledge',
|
||||
'Chunking',
|
||||
'MCP'
|
||||
'MCP',
|
||||
'GuardrailResult',
|
||||
'LLMGuardrail'
|
||||
]
|
||||
@@ -3,7 +3,7 @@ import time
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import List, Optional, Any, Dict, Union, Literal, TYPE_CHECKING
|
||||
from typing import List, Optional, Any, Dict, Union, Literal, TYPE_CHECKING, Callable, Tuple
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
from openai import AsyncOpenAI
|
||||
@@ -32,6 +32,7 @@ _shared_apps = {} # Dict of port -> FastAPI app
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..task.task import Task
|
||||
from ..main import TaskOutput
|
||||
|
||||
@dataclass
|
||||
class ChatCompletionMessage:
|
||||
@@ -368,7 +369,9 @@ class Agent:
|
||||
min_reflect: int = 1,
|
||||
reflect_llm: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
reasoning_steps: bool = False
|
||||
reasoning_steps: bool = False,
|
||||
guardrail: Optional[Union[Callable[['TaskOutput'], Tuple[bool, Any]], str]] = None,
|
||||
max_guardrail_retries: int = 3
|
||||
):
|
||||
# Add check at start if memory is requested
|
||||
if memory is not None:
|
||||
@@ -483,6 +486,12 @@ Your Goal: {self.goal}
|
||||
# Store user_id
|
||||
self.user_id = user_id or "praison"
|
||||
self.reasoning_steps = reasoning_steps
|
||||
|
||||
# Initialize guardrail settings
|
||||
self.guardrail = guardrail
|
||||
self.max_guardrail_retries = max_guardrail_retries
|
||||
self._guardrail_fn = None
|
||||
self._setup_guardrail()
|
||||
|
||||
# Check if knowledge parameter has any values
|
||||
if not knowledge:
|
||||
@@ -512,6 +521,152 @@ Your Goal: {self.goal}
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing knowledge item: {knowledge_item}, error: {e}")
|
||||
|
||||
def _setup_guardrail(self):
|
||||
"""Setup the guardrail function based on the provided guardrail parameter."""
|
||||
if self.guardrail is None:
|
||||
self._guardrail_fn = None
|
||||
return
|
||||
|
||||
if callable(self.guardrail):
|
||||
# Validate function signature
|
||||
sig = inspect.signature(self.guardrail)
|
||||
positional_args = [
|
||||
param for param in sig.parameters.values()
|
||||
if param.default is inspect.Parameter.empty
|
||||
]
|
||||
if len(positional_args) != 1:
|
||||
raise ValueError("Agent guardrail function must accept exactly one parameter (TaskOutput)")
|
||||
|
||||
# Check return annotation if present
|
||||
from typing import get_args, get_origin
|
||||
return_annotation = sig.return_annotation
|
||||
if return_annotation != inspect.Signature.empty:
|
||||
return_annotation_args = get_args(return_annotation)
|
||||
if not (
|
||||
get_origin(return_annotation) is tuple
|
||||
and len(return_annotation_args) == 2
|
||||
and return_annotation_args[0] is bool
|
||||
and (
|
||||
return_annotation_args[1] is Any
|
||||
or return_annotation_args[1] is str
|
||||
or str(return_annotation_args[1]).endswith('TaskOutput')
|
||||
or str(return_annotation_args[1]).startswith('typing.Union')
|
||||
)
|
||||
):
|
||||
raise ValueError(
|
||||
"If return type is annotated, it must be Tuple[bool, Any] or Tuple[bool, Union[str, TaskOutput]]"
|
||||
)
|
||||
|
||||
self._guardrail_fn = self.guardrail
|
||||
elif isinstance(self.guardrail, str):
|
||||
# Create LLM-based guardrail
|
||||
from ..guardrails import LLMGuardrail
|
||||
llm = getattr(self, 'llm', None) or getattr(self, 'llm_instance', None)
|
||||
self._guardrail_fn = LLMGuardrail(description=self.guardrail, llm=llm)
|
||||
else:
|
||||
raise ValueError("Agent guardrail must be either a callable or a string description")
|
||||
|
||||
def _process_guardrail(self, task_output):
|
||||
"""Process the guardrail validation for a task output.
|
||||
|
||||
Args:
|
||||
task_output: The task output to validate
|
||||
|
||||
Returns:
|
||||
GuardrailResult: The result of the guardrail validation
|
||||
"""
|
||||
from ..guardrails import GuardrailResult
|
||||
|
||||
if not self._guardrail_fn:
|
||||
return GuardrailResult(success=True, result=task_output)
|
||||
|
||||
try:
|
||||
# Call the guardrail function
|
||||
result = self._guardrail_fn(task_output)
|
||||
|
||||
# Convert the result to a GuardrailResult
|
||||
return GuardrailResult.from_tuple(result)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Agent {self.name}: Error in guardrail validation: {e}")
|
||||
# On error, return failure
|
||||
return GuardrailResult(
|
||||
success=False,
|
||||
result=None,
|
||||
error=f"Agent guardrail validation error: {str(e)}"
|
||||
)
|
||||
|
||||
def _apply_guardrail_with_retry(self, response_text, prompt, temperature=0.2, tools=None):
|
||||
"""Apply guardrail validation with retry logic.
|
||||
|
||||
Args:
|
||||
response_text: The response to validate
|
||||
prompt: Original prompt for regeneration if needed
|
||||
temperature: Temperature for regeneration
|
||||
tools: Tools for regeneration
|
||||
|
||||
Returns:
|
||||
str: The validated response text or None if validation fails after retries
|
||||
"""
|
||||
if not self._guardrail_fn:
|
||||
return response_text
|
||||
|
||||
from ..main import TaskOutput
|
||||
|
||||
retry_count = 0
|
||||
current_response = response_text
|
||||
|
||||
while retry_count <= self.max_guardrail_retries:
|
||||
# Create TaskOutput object
|
||||
task_output = TaskOutput(
|
||||
raw=current_response,
|
||||
output=current_response,
|
||||
pydantic=None,
|
||||
json_dict=None,
|
||||
name=f"{self.name}_output",
|
||||
description="Agent response output"
|
||||
)
|
||||
|
||||
# Process guardrail
|
||||
guardrail_result = self._process_guardrail(task_output)
|
||||
|
||||
if guardrail_result.success:
|
||||
logging.info(f"Agent {self.name}: Guardrail validation passed")
|
||||
# Return the potentially modified result
|
||||
if guardrail_result.result and hasattr(guardrail_result.result, 'raw'):
|
||||
return guardrail_result.result.raw
|
||||
elif guardrail_result.result:
|
||||
return str(guardrail_result.result)
|
||||
else:
|
||||
return current_response
|
||||
|
||||
# Guardrail failed
|
||||
if retry_count >= self.max_guardrail_retries:
|
||||
raise Exception(
|
||||
f"Agent {self.name} response failed guardrail validation after {self.max_guardrail_retries} retries. "
|
||||
f"Last error: {guardrail_result.error}"
|
||||
)
|
||||
|
||||
retry_count += 1
|
||||
logging.warning(f"Agent {self.name}: Guardrail validation failed (retry {retry_count}/{self.max_guardrail_retries}): {guardrail_result.error}")
|
||||
|
||||
# Regenerate response for retry
|
||||
try:
|
||||
retry_prompt = f"{prompt}\n\nNote: Previous response failed validation due to: {guardrail_result.error}. Please provide an improved response."
|
||||
response = self._chat_completion([{"role": "user", "content": retry_prompt}], temperature, tools)
|
||||
if response and response.choices:
|
||||
current_response = response.choices[0].message.content.strip()
|
||||
else:
|
||||
raise Exception("Failed to generate retry response")
|
||||
except Exception as e:
|
||||
logging.error(f"Agent {self.name}: Error during guardrail retry: {e}")
|
||||
# If we can't regenerate, fail the guardrail
|
||||
raise Exception(
|
||||
f"Agent {self.name} guardrail retry failed: {e}"
|
||||
)
|
||||
|
||||
return current_response
|
||||
|
||||
def generate_task(self) -> 'Task':
|
||||
"""Generate a Task object from the agent's instructions"""
|
||||
from ..task.task import Task
|
||||
@@ -788,69 +943,92 @@ Your Goal: {self.goal}
|
||||
)
|
||||
else:
|
||||
# Use the standard OpenAI client approach
|
||||
if stream:
|
||||
# Process as streaming response with formatted tools
|
||||
final_response = self._process_stream_response(
|
||||
messages,
|
||||
temperature,
|
||||
start_time,
|
||||
formatted_tools=formatted_tools if formatted_tools else None,
|
||||
reasoning_steps=reasoning_steps
|
||||
)
|
||||
else:
|
||||
# Process as regular non-streaming response
|
||||
final_response = client.chat.completions.create(
|
||||
model=self.llm,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
tools=formatted_tools if formatted_tools else None,
|
||||
stream=False
|
||||
)
|
||||
# Continue tool execution loop until no more tool calls are needed
|
||||
max_iterations = 10 # Prevent infinite loops
|
||||
iteration_count = 0
|
||||
|
||||
while iteration_count < max_iterations:
|
||||
if stream:
|
||||
# Process as streaming response with formatted tools
|
||||
final_response = self._process_stream_response(
|
||||
messages,
|
||||
temperature,
|
||||
start_time,
|
||||
formatted_tools=formatted_tools if formatted_tools else None,
|
||||
reasoning_steps=reasoning_steps
|
||||
)
|
||||
else:
|
||||
# Process as regular non-streaming response
|
||||
final_response = client.chat.completions.create(
|
||||
model=self.llm,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
tools=formatted_tools if formatted_tools else None,
|
||||
stream=False
|
||||
)
|
||||
|
||||
tool_calls = getattr(final_response.choices[0].message, 'tool_calls', None)
|
||||
tool_calls = getattr(final_response.choices[0].message, 'tool_calls', None)
|
||||
|
||||
if tool_calls:
|
||||
messages.append({
|
||||
"role": "assistant",
|
||||
"content": final_response.choices[0].message.content,
|
||||
"tool_calls": tool_calls
|
||||
})
|
||||
if tool_calls:
|
||||
messages.append({
|
||||
"role": "assistant",
|
||||
"content": final_response.choices[0].message.content,
|
||||
"tool_calls": tool_calls
|
||||
})
|
||||
|
||||
for tool_call in tool_calls:
|
||||
function_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
for tool_call in tool_calls:
|
||||
function_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
|
||||
if self.verbose:
|
||||
display_tool_call(f"Agent {self.name} is calling function '{function_name}' with arguments: {arguments}")
|
||||
if self.verbose:
|
||||
display_tool_call(f"Agent {self.name} is calling function '{function_name}' with arguments: {arguments}")
|
||||
|
||||
tool_result = self.execute_tool(function_name, arguments)
|
||||
results_str = json.dumps(tool_result) if tool_result else "Function returned an empty output"
|
||||
tool_result = self.execute_tool(function_name, arguments)
|
||||
results_str = json.dumps(tool_result) if tool_result else "Function returned an empty output"
|
||||
|
||||
if self.verbose:
|
||||
display_tool_call(f"Function '{function_name}' returned: {results_str}")
|
||||
if self.verbose:
|
||||
display_tool_call(f"Function '{function_name}' returned: {results_str}")
|
||||
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": results_str
|
||||
})
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": results_str
|
||||
})
|
||||
|
||||
# Get final response after tool calls
|
||||
if stream:
|
||||
final_response = self._process_stream_response(
|
||||
messages,
|
||||
temperature,
|
||||
start_time,
|
||||
formatted_tools=formatted_tools if formatted_tools else None,
|
||||
reasoning_steps=reasoning_steps
|
||||
)
|
||||
else:
|
||||
final_response = client.chat.completions.create(
|
||||
model=self.llm,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
stream=False
|
||||
)
|
||||
# Check if we should continue (for tools like sequential thinking)
|
||||
should_continue = False
|
||||
for tool_call in tool_calls:
|
||||
function_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
|
||||
# For sequential thinking tool, check if nextThoughtNeeded is True
|
||||
if function_name == "sequentialthinking" and arguments.get("nextThoughtNeeded", False):
|
||||
should_continue = True
|
||||
break
|
||||
|
||||
if not should_continue:
|
||||
# Get final response after tool calls
|
||||
if stream:
|
||||
final_response = self._process_stream_response(
|
||||
messages,
|
||||
temperature,
|
||||
start_time,
|
||||
formatted_tools=formatted_tools if formatted_tools else None,
|
||||
reasoning_steps=reasoning_steps
|
||||
)
|
||||
else:
|
||||
final_response = client.chat.completions.create(
|
||||
model=self.llm,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
stream=False
|
||||
)
|
||||
break
|
||||
|
||||
iteration_count += 1
|
||||
else:
|
||||
# No tool calls, we're done
|
||||
break
|
||||
|
||||
return final_response
|
||||
|
||||
@@ -944,7 +1122,13 @@ Your Goal: {self.goal}
|
||||
total_time = time.time() - start_time
|
||||
logging.debug(f"Agent.chat completed in {total_time:.2f} seconds")
|
||||
|
||||
return response_text
|
||||
# Apply guardrail validation for custom LLM response
|
||||
try:
|
||||
validated_response = self._apply_guardrail_with_retry(response_text, prompt, temperature, tools)
|
||||
return validated_response
|
||||
except Exception as e:
|
||||
logging.error(f"Agent {self.name}: Guardrail validation failed for custom LLM: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
display_error(f"Error in LLM chat: {e}")
|
||||
return None
|
||||
@@ -1032,8 +1216,20 @@ Your Goal: {self.goal}
|
||||
display_interaction(original_prompt, response_text, markdown=self.markdown, generation_time=time.time() - start_time, console=self.console)
|
||||
# Return only reasoning content if reasoning_steps is True
|
||||
if reasoning_steps and hasattr(response.choices[0].message, 'reasoning_content'):
|
||||
return response.choices[0].message.reasoning_content
|
||||
return response_text
|
||||
# Apply guardrail to reasoning content
|
||||
try:
|
||||
validated_reasoning = self._apply_guardrail_with_retry(response.choices[0].message.reasoning_content, original_prompt, temperature, tools)
|
||||
return validated_reasoning
|
||||
except Exception as e:
|
||||
logging.error(f"Agent {self.name}: Guardrail validation failed for reasoning content: {e}")
|
||||
return None
|
||||
# Apply guardrail to regular response
|
||||
try:
|
||||
validated_response = self._apply_guardrail_with_retry(response_text, original_prompt, temperature, tools)
|
||||
return validated_response
|
||||
except Exception as e:
|
||||
logging.error(f"Agent {self.name}: Guardrail validation failed: {e}")
|
||||
return None
|
||||
|
||||
reflection_prompt = f"""
|
||||
Reflect on your previous response: '{response_text}'.
|
||||
@@ -1066,7 +1262,13 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
self.chat_history.append({"role": "user", "content": prompt})
|
||||
self.chat_history.append({"role": "assistant", "content": response_text})
|
||||
display_interaction(prompt, response_text, markdown=self.markdown, generation_time=time.time() - start_time, console=self.console)
|
||||
return response_text
|
||||
# Apply guardrail validation after satisfactory reflection
|
||||
try:
|
||||
validated_response = self._apply_guardrail_with_retry(response_text, prompt, temperature, tools)
|
||||
return validated_response
|
||||
except Exception as e:
|
||||
logging.error(f"Agent {self.name}: Guardrail validation failed after reflection: {e}")
|
||||
return None
|
||||
|
||||
# Check if we've hit max reflections
|
||||
if reflection_count >= self.max_reflect - 1:
|
||||
@@ -1075,7 +1277,13 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
self.chat_history.append({"role": "user", "content": prompt})
|
||||
self.chat_history.append({"role": "assistant", "content": response_text})
|
||||
display_interaction(prompt, response_text, markdown=self.markdown, generation_time=time.time() - start_time, console=self.console)
|
||||
return response_text
|
||||
# Apply guardrail validation after max reflections
|
||||
try:
|
||||
validated_response = self._apply_guardrail_with_retry(response_text, prompt, temperature, tools)
|
||||
return validated_response
|
||||
except Exception as e:
|
||||
logging.error(f"Agent {self.name}: Guardrail validation failed after max reflections: {e}")
|
||||
return None
|
||||
|
||||
logging.debug(f"{self.name} reflection count {reflection_count + 1}, continuing reflection process")
|
||||
messages.append({"role": "user", "content": "Now regenerate your response using the reflection you made"})
|
||||
@@ -1099,8 +1307,16 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
|
||||
total_time = time.time() - start_time
|
||||
logging.debug(f"Agent.chat completed in {total_time:.2f} seconds")
|
||||
|
||||
return response_text
|
||||
|
||||
# Apply guardrail validation before returning
|
||||
try:
|
||||
validated_response = self._apply_guardrail_with_retry(response_text, prompt, temperature, tools)
|
||||
return validated_response
|
||||
except Exception as e:
|
||||
logging.error(f"Agent {self.name}: Guardrail validation failed: {e}")
|
||||
if self.verbose:
|
||||
display_error(f"Guardrail validation failed: {e}", console=self.console)
|
||||
return None
|
||||
|
||||
def clean_json_output(self, output: str) -> str:
|
||||
"""Clean and extract JSON from response text."""
|
||||
@@ -1670,6 +1886,7 @@ Your Goal: {self.goal}
|
||||
import threading
|
||||
import time
|
||||
import inspect
|
||||
import asyncio # Import asyncio in the MCP scope
|
||||
# logging is already imported at the module level
|
||||
|
||||
except ImportError as e:
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Guardrails module for PraisonAI Agents.
|
||||
|
||||
This module provides validation and safety mechanisms for task outputs,
|
||||
including both function-based and LLM-based guardrails.
|
||||
"""
|
||||
|
||||
from .guardrail_result import GuardrailResult
|
||||
from .llm_guardrail import LLMGuardrail
|
||||
|
||||
__all__ = ["GuardrailResult", "LLMGuardrail"]
|
||||
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Guardrail result classes for PraisonAI Agents.
|
||||
|
||||
This module provides the result types for guardrail validation,
|
||||
following the same pattern as CrewAI for consistency.
|
||||
"""
|
||||
|
||||
from typing import Any, Tuple, Union
|
||||
from pydantic import BaseModel, Field
|
||||
from ..main import TaskOutput
|
||||
|
||||
|
||||
class GuardrailResult(BaseModel):
|
||||
"""Result of a guardrail validation."""
|
||||
|
||||
success: bool = Field(description="Whether the guardrail check passed")
|
||||
result: Union[str, TaskOutput, None] = Field(description="The result if modified, or None if unchanged")
|
||||
error: str = Field(default="", description="Error message if validation failed")
|
||||
|
||||
@classmethod
|
||||
def from_tuple(cls, result: Tuple[bool, Any]) -> "GuardrailResult":
|
||||
"""Create a GuardrailResult from a tuple returned by a guardrail function.
|
||||
|
||||
Args:
|
||||
result: Tuple of (success, result_or_error)
|
||||
|
||||
Returns:
|
||||
GuardrailResult: The structured result
|
||||
"""
|
||||
success, data = result
|
||||
|
||||
if success:
|
||||
return cls(
|
||||
success=True,
|
||||
result=data,
|
||||
error=""
|
||||
)
|
||||
else:
|
||||
return cls(
|
||||
success=False,
|
||||
result=None,
|
||||
error=str(data) if data else "Guardrail validation failed"
|
||||
)
|
||||
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
LLM-based guardrail implementation for PraisonAI Agents.
|
||||
|
||||
This module provides LLM-powered guardrails that can validate task outputs
|
||||
using natural language descriptions, similar to CrewAI's implementation.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Tuple, Union, Optional
|
||||
from pydantic import BaseModel
|
||||
from ..main import TaskOutput
|
||||
|
||||
|
||||
class LLMGuardrail:
|
||||
"""An LLM-powered guardrail that validates task outputs using natural language."""
|
||||
|
||||
def __init__(self, description: str, llm: Any = None):
|
||||
"""Initialize the LLM guardrail.
|
||||
|
||||
Args:
|
||||
description: Natural language description of what to validate
|
||||
llm: The LLM instance to use for validation
|
||||
"""
|
||||
self.description = description
|
||||
self.llm = llm
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def __call__(self, task_output: TaskOutput) -> Tuple[bool, Union[str, TaskOutput]]:
|
||||
"""Validate the task output using the LLM.
|
||||
|
||||
Args:
|
||||
task_output: The task output to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (success, result) where result is the output or error message
|
||||
"""
|
||||
try:
|
||||
if not self.llm:
|
||||
self.logger.warning("No LLM provided for guardrail validation")
|
||||
return True, task_output
|
||||
|
||||
# Create validation prompt
|
||||
validation_prompt = f"""
|
||||
You are a quality assurance validator. Your task is to evaluate the following output against specific criteria.
|
||||
|
||||
Validation Criteria: {self.description}
|
||||
|
||||
Output to Validate:
|
||||
{task_output.raw}
|
||||
|
||||
Please evaluate if this output meets the criteria. Respond with:
|
||||
1. "PASS" if the output meets all criteria
|
||||
2. "FAIL: [specific reason]" if the output does not meet criteria
|
||||
|
||||
Your response:"""
|
||||
|
||||
# Get LLM response
|
||||
if hasattr(self.llm, 'chat'):
|
||||
# For Agent's LLM interface
|
||||
response = self.llm.chat(validation_prompt, temperature=0.1)
|
||||
elif hasattr(self.llm, 'get_response'):
|
||||
# For custom LLM instances
|
||||
response = self.llm.get_response(validation_prompt, temperature=0.1)
|
||||
elif callable(self.llm):
|
||||
# For simple callable LLMs
|
||||
response = self.llm(validation_prompt)
|
||||
else:
|
||||
self.logger.error(f"Unsupported LLM type: {type(self.llm)}")
|
||||
return True, task_output
|
||||
|
||||
# Parse response
|
||||
response = str(response).strip()
|
||||
|
||||
if response.upper().startswith("PASS"):
|
||||
return True, task_output
|
||||
elif response.upper().startswith("FAIL"):
|
||||
# Extract the reason
|
||||
reason = response[5:].strip(": ")
|
||||
return False, f"Guardrail validation failed: {reason}"
|
||||
else:
|
||||
# Unclear response, log and pass through
|
||||
self.logger.warning(f"Unclear guardrail response: {response}")
|
||||
return True, task_output
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in LLM guardrail validation: {str(e)}")
|
||||
# On error, pass through the original output
|
||||
return True, task_output
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import List, Optional, Dict, Any, Type, Callable, Union, Coroutine, Literal
|
||||
import inspect
|
||||
from typing import List, Optional, Dict, Any, Type, Callable, Union, Coroutine, Literal, Tuple, get_args, get_origin
|
||||
from pydantic import BaseModel
|
||||
from ..main import TaskOutput
|
||||
from ..agent.agent import Agent
|
||||
@@ -40,7 +41,10 @@ class Task:
|
||||
quality_check=True,
|
||||
input_file: Optional[str] = None,
|
||||
rerun: bool = False, # Renamed from can_rerun and logic inverted, default True for backward compatibility
|
||||
retain_full_context: bool = False # By default, only use previous task output, not all previous tasks
|
||||
retain_full_context: bool = False, # By default, only use previous task output, not all previous tasks
|
||||
guardrail: Optional[Union[Callable[[TaskOutput], Tuple[bool, Any]], str]] = None,
|
||||
max_retries: int = 3,
|
||||
retry_count: int = 0
|
||||
):
|
||||
# Add check if memory config is provided
|
||||
if memory is not None or (config and config.get('memory_config')):
|
||||
@@ -80,6 +84,10 @@ class Task:
|
||||
self.quality_check = quality_check
|
||||
self.rerun = rerun # Assigning the rerun parameter
|
||||
self.retain_full_context = retain_full_context
|
||||
self.guardrail = guardrail
|
||||
self.max_retries = max_retries
|
||||
self.retry_count = retry_count
|
||||
self._guardrail_fn = None
|
||||
|
||||
# Set logger level based on config verbose level
|
||||
verbose = self.config.get("verbose", 0)
|
||||
@@ -141,6 +149,55 @@ class Task:
|
||||
|
||||
self.output_pydantic = LoopModel
|
||||
|
||||
# Initialize guardrail
|
||||
self._setup_guardrail()
|
||||
|
||||
def _setup_guardrail(self):
|
||||
"""Setup the guardrail function based on the provided guardrail parameter."""
|
||||
if self.guardrail is None:
|
||||
self._guardrail_fn = None
|
||||
return
|
||||
|
||||
if callable(self.guardrail):
|
||||
# Validate function signature
|
||||
sig = inspect.signature(self.guardrail)
|
||||
positional_args = [
|
||||
param for param in sig.parameters.values()
|
||||
if param.default is inspect.Parameter.empty
|
||||
]
|
||||
if len(positional_args) != 1:
|
||||
raise ValueError("Guardrail function must accept exactly one parameter (TaskOutput)")
|
||||
|
||||
# Check return annotation if present
|
||||
return_annotation = sig.return_annotation
|
||||
if return_annotation != inspect.Signature.empty:
|
||||
return_annotation_args = get_args(return_annotation)
|
||||
if not (
|
||||
get_origin(return_annotation) is tuple
|
||||
and len(return_annotation_args) == 2
|
||||
and return_annotation_args[0] is bool
|
||||
and (
|
||||
return_annotation_args[1] is Any
|
||||
or return_annotation_args[1] is str
|
||||
or return_annotation_args[1] is TaskOutput
|
||||
or return_annotation_args[1] == Union[str, TaskOutput]
|
||||
)
|
||||
):
|
||||
raise ValueError(
|
||||
"If return type is annotated, it must be Tuple[bool, Any]"
|
||||
)
|
||||
|
||||
self._guardrail_fn = self.guardrail
|
||||
elif isinstance(self.guardrail, str):
|
||||
# Create LLM-based guardrail
|
||||
from ..guardrails import LLMGuardrail
|
||||
if not self.agent:
|
||||
raise ValueError("Agent is required for string-based guardrails")
|
||||
llm = getattr(self.agent, 'llm', None) or getattr(self.agent, 'llm_instance', None)
|
||||
self._guardrail_fn = LLMGuardrail(description=self.guardrail, llm=llm)
|
||||
else:
|
||||
raise ValueError("Guardrail must be either a callable or a string description")
|
||||
|
||||
def __str__(self):
|
||||
return f"Task(name='{self.name if self.name else 'None'}', description='{self.description}', agent='{self.agent.name if self.agent else 'None'}', status='{self.status}')"
|
||||
|
||||
@@ -187,6 +244,37 @@ class Task:
|
||||
logger.info(f"Task {self.id}: execute_callback called")
|
||||
logger.info(f"Quality check enabled: {self.quality_check}")
|
||||
|
||||
# Process guardrail if configured
|
||||
if self._guardrail_fn:
|
||||
try:
|
||||
guardrail_result = self._process_guardrail(task_output)
|
||||
if not guardrail_result.success:
|
||||
if self.retry_count >= self.max_retries:
|
||||
raise Exception(
|
||||
f"Task failed guardrail validation after {self.max_retries} retries. "
|
||||
f"Last error: {guardrail_result.error}"
|
||||
)
|
||||
|
||||
self.retry_count += 1
|
||||
logger.warning(f"Task {self.id}: Guardrail validation failed (retry {self.retry_count}/{self.max_retries}): {guardrail_result.error}")
|
||||
# Note: In a real execution, this would trigger a retry, but since this is a callback
|
||||
# the retry logic would need to be handled at the agent/execution level
|
||||
return
|
||||
|
||||
# If guardrail passed and returned a modified result
|
||||
if guardrail_result.result is not None:
|
||||
if isinstance(guardrail_result.result, str):
|
||||
# Update the task output with the modified result
|
||||
task_output.raw = guardrail_result.result
|
||||
elif isinstance(guardrail_result.result, TaskOutput):
|
||||
# Replace with the new task output
|
||||
task_output = guardrail_result.result
|
||||
|
||||
logger.info(f"Task {self.id}: Guardrail validation passed")
|
||||
except Exception as e:
|
||||
logger.error(f"Task {self.id}: Error in guardrail processing: {e}")
|
||||
# Continue execution even if guardrail fails to avoid breaking the task
|
||||
|
||||
# Initialize memory if not already initialized
|
||||
if not self.memory:
|
||||
self.memory = self.initialize_memory()
|
||||
@@ -334,4 +422,34 @@ Context:
|
||||
loop.run_until_complete(self.execute_callback(task_output))
|
||||
except RuntimeError:
|
||||
# If no loop is running in this context
|
||||
asyncio.run(self.execute_callback(task_output))
|
||||
asyncio.run(self.execute_callback(task_output))
|
||||
|
||||
def _process_guardrail(self, task_output: TaskOutput):
|
||||
"""Process the guardrail validation for a task output.
|
||||
|
||||
Args:
|
||||
task_output: The task output to validate
|
||||
|
||||
Returns:
|
||||
GuardrailResult: The result of the guardrail validation
|
||||
"""
|
||||
from ..guardrails import GuardrailResult
|
||||
|
||||
if not self._guardrail_fn:
|
||||
return GuardrailResult(success=True, result=task_output)
|
||||
|
||||
try:
|
||||
# Call the guardrail function
|
||||
result = self._guardrail_fn(task_output)
|
||||
|
||||
# Convert the result to a GuardrailResult
|
||||
return GuardrailResult.from_tuple(result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Task {self.id}: Error in guardrail validation: {e}")
|
||||
# On error, return failure
|
||||
return GuardrailResult(
|
||||
success=False,
|
||||
result=None,
|
||||
error=f"Guardrail validation error: {str(e)}"
|
||||
)
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.97"
|
||||
version = "0.0.98"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from praisonaiagents import Agent, MCP
|
||||
import os
|
||||
|
||||
sequential_agent = Agent(
|
||||
instructions="""You are a helpful assistant that can break down complex problems.
|
||||
Use the available tools when relevant to perform step-by-step analysis.""",
|
||||
llm="openai/gpt-4o-mini",
|
||||
tools=MCP("npx -y @modelcontextprotocol/server-sequential-thinking")
|
||||
)
|
||||
|
||||
sequential_agent.start("Break down the process of making a cup of tea")
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test for guardrails functionality without full dependencies.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from typing import Tuple, Any
|
||||
|
||||
# Import only what we need for testing
|
||||
from praisonaiagents.guardrails import GuardrailResult, LLMGuardrail
|
||||
from praisonaiagents.main import TaskOutput
|
||||
|
||||
|
||||
def test_guardrail_result():
|
||||
"""Test GuardrailResult helper methods."""
|
||||
print("Testing GuardrailResult...")
|
||||
|
||||
# Test success case
|
||||
success_result = GuardrailResult.from_tuple((True, "Modified output"))
|
||||
assert success_result.success
|
||||
assert success_result.result == "Modified output"
|
||||
assert success_result.error == ""
|
||||
print("✓ Success result created correctly")
|
||||
|
||||
# Test failure case
|
||||
failure_result = GuardrailResult.from_tuple((False, "Validation failed"))
|
||||
assert not failure_result.success
|
||||
assert failure_result.result is None
|
||||
assert failure_result.error == "Validation failed"
|
||||
print("✓ Failure result created correctly")
|
||||
|
||||
print("GuardrailResult test passed!\n")
|
||||
|
||||
|
||||
def test_function_guardrail():
|
||||
"""Test function-based guardrail logic."""
|
||||
print("Testing function-based guardrail logic...")
|
||||
|
||||
def validate_output(task_output: TaskOutput) -> Tuple[bool, Any]:
|
||||
"""Simple validation function."""
|
||||
if "error" in task_output.raw.lower():
|
||||
return False, "Output contains errors"
|
||||
if len(task_output.raw) < 10:
|
||||
return False, "Output is too short"
|
||||
return True, task_output
|
||||
|
||||
# Test with good output
|
||||
good_output = TaskOutput(
|
||||
description="Test task",
|
||||
raw="Hello! This is a friendly greeting message from the agent.",
|
||||
agent="Test Agent"
|
||||
)
|
||||
|
||||
result = validate_output(good_output)
|
||||
guardrail_result = GuardrailResult.from_tuple(result)
|
||||
assert guardrail_result.success, f"Good output should pass: {guardrail_result.error}"
|
||||
print("✓ Good output passed function guardrail")
|
||||
|
||||
# Test with bad output
|
||||
bad_output = TaskOutput(
|
||||
description="Test task",
|
||||
raw="Error occurred",
|
||||
agent="Test Agent"
|
||||
)
|
||||
|
||||
result = validate_output(bad_output)
|
||||
guardrail_result = GuardrailResult.from_tuple(result)
|
||||
assert not guardrail_result.success, "Bad output should fail guardrail"
|
||||
print("✓ Bad output failed function guardrail as expected")
|
||||
|
||||
print("Function-based guardrail logic test passed!\n")
|
||||
|
||||
|
||||
def test_llm_guardrail():
|
||||
"""Test LLM guardrail logic."""
|
||||
print("Testing LLM guardrail logic...")
|
||||
|
||||
# Mock LLM for testing that correctly parses the validation prompt
|
||||
class MockLLM:
|
||||
def chat(self, prompt, **kwargs):
|
||||
# Extract the actual output to validate from the prompt
|
||||
# The LLMGuardrail sends a structured prompt with "Output to Validate:" section
|
||||
if "Output to Validate:" in prompt:
|
||||
# Split by "Output to Validate:" and get the content after it
|
||||
parts = prompt.split("Output to Validate:")
|
||||
if len(parts) > 1:
|
||||
output_content = parts[1].strip()
|
||||
# Check only the output content, not the validation criteria
|
||||
if "error" in output_content.lower():
|
||||
return "FAIL: The output contains error messages"
|
||||
return "PASS"
|
||||
|
||||
# Fallback: if no "Output to Validate:" section, return pass
|
||||
return "PASS"
|
||||
|
||||
# Create LLM guardrail
|
||||
mock_llm = MockLLM()
|
||||
llm_guardrail = LLMGuardrail(
|
||||
description="Check if the output is professional and does not contain errors",
|
||||
llm=mock_llm
|
||||
)
|
||||
|
||||
# Test with good output
|
||||
good_output = TaskOutput(
|
||||
description="Test task",
|
||||
raw="Hello! This is a professional greeting message.",
|
||||
agent="Test Agent"
|
||||
)
|
||||
|
||||
result = llm_guardrail(good_output)
|
||||
guardrail_result = GuardrailResult.from_tuple(result)
|
||||
assert guardrail_result.success, f"Good output should pass: {guardrail_result.error}"
|
||||
print("✓ Good output passed LLM guardrail")
|
||||
|
||||
# Test with bad output
|
||||
bad_output = TaskOutput(
|
||||
description="Test task",
|
||||
raw="There was an error in the system",
|
||||
agent="Test Agent"
|
||||
)
|
||||
|
||||
result = llm_guardrail(bad_output)
|
||||
guardrail_result = GuardrailResult.from_tuple(result)
|
||||
assert not guardrail_result.success, "Bad output should fail LLM guardrail"
|
||||
print("✓ Bad output failed LLM guardrail as expected")
|
||||
|
||||
print("LLM guardrail logic test passed!\n")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all tests."""
|
||||
print("Running Simple Guardrails Tests...\n")
|
||||
|
||||
try:
|
||||
test_guardrail_result()
|
||||
test_function_guardrail()
|
||||
test_llm_guardrail()
|
||||
|
||||
print("🎉 All simple guardrail tests passed!")
|
||||
print("\nGuardrails core logic is working correctly!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test for Agent guardrail functionality.
|
||||
"""
|
||||
|
||||
def test_agent_guardrails():
|
||||
"""Test basic Agent guardrail functionality."""
|
||||
print("Testing Agent guardrail functionality...")
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent, TaskOutput
|
||||
print("✓ Basic imports successful")
|
||||
|
||||
# Test function-based guardrail
|
||||
def test_guardrail(task_output: TaskOutput):
|
||||
if len(task_output.raw) < 10:
|
||||
return False, "Too short"
|
||||
return True, task_output
|
||||
|
||||
# Test Agent creation with function guardrail
|
||||
agent1 = Agent(
|
||||
name="TestAgent1",
|
||||
instructions="You are a test agent",
|
||||
guardrail=test_guardrail,
|
||||
max_guardrail_retries=2
|
||||
)
|
||||
print("✓ Agent with function guardrail created successfully")
|
||||
print(f" - Agent name: {agent1.name}")
|
||||
print(f" - Has guardrail function: {agent1._guardrail_fn is not None}")
|
||||
print(f" - Max retries: {agent1.max_guardrail_retries}")
|
||||
|
||||
# Test Agent creation with string guardrail
|
||||
agent2 = Agent(
|
||||
name="TestAgent2",
|
||||
instructions="You are a test agent",
|
||||
guardrail="Ensure the response is polite and professional",
|
||||
max_guardrail_retries=3
|
||||
)
|
||||
print("✓ Agent with LLM guardrail created successfully")
|
||||
print(f" - Agent name: {agent2.name}")
|
||||
print(f" - Has guardrail function: {agent2._guardrail_fn is not None}")
|
||||
print(f" - Max retries: {agent2.max_guardrail_retries}")
|
||||
|
||||
# Test Agent without guardrail
|
||||
agent3 = Agent(
|
||||
name="TestAgent3",
|
||||
instructions="You are a test agent"
|
||||
)
|
||||
print("✓ Agent without guardrail created successfully")
|
||||
print(f" - Agent name: {agent3.name}")
|
||||
print(f" - Has guardrail function: {agent3._guardrail_fn is None}")
|
||||
|
||||
print("\n🎉 All Agent guardrail tests passed!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_agent_guardrails()
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for guardrails functionality in PraisonAI Agents.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from praisonaiagents import Agent, Task, TaskOutput
|
||||
from praisonaiagents.guardrails import GuardrailResult, LLMGuardrail
|
||||
from typing import Tuple, Any
|
||||
|
||||
|
||||
def test_function_guardrail():
|
||||
"""Test function-based guardrail."""
|
||||
print("Testing function-based guardrail...")
|
||||
|
||||
def validate_output(task_output: TaskOutput) -> Tuple[bool, Any]:
|
||||
"""Simple validation function."""
|
||||
if "error" in task_output.raw.lower():
|
||||
return False, "Output contains errors"
|
||||
if len(task_output.raw) < 10:
|
||||
return False, "Output is too short"
|
||||
return True, task_output
|
||||
|
||||
# Create agent and task with guardrail
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
role="Tester",
|
||||
goal="Test guardrails",
|
||||
backstory="I am testing the guardrail functionality"
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Write a simple hello message",
|
||||
expected_output="A friendly greeting message",
|
||||
agent=agent,
|
||||
guardrail=validate_output,
|
||||
max_retries=2
|
||||
)
|
||||
|
||||
# Test with good output
|
||||
good_output = TaskOutput(
|
||||
description="Test task",
|
||||
raw="Hello! This is a friendly greeting message from the agent.",
|
||||
agent="Test Agent"
|
||||
)
|
||||
|
||||
result = task._process_guardrail(good_output)
|
||||
assert result.success, f"Good output should pass: {result.error}"
|
||||
print("✓ Good output passed guardrail")
|
||||
|
||||
# Test with bad output
|
||||
bad_output = TaskOutput(
|
||||
description="Test task",
|
||||
raw="Error occurred",
|
||||
agent="Test Agent"
|
||||
)
|
||||
|
||||
result = task._process_guardrail(bad_output)
|
||||
assert not result.success, "Bad output should fail guardrail"
|
||||
print("✓ Bad output failed guardrail as expected")
|
||||
|
||||
print("Function-based guardrail test passed!\n")
|
||||
|
||||
|
||||
def test_string_guardrail():
|
||||
"""Test string-based LLM guardrail."""
|
||||
print("Testing string-based LLM guardrail...")
|
||||
|
||||
# Mock LLM for testing
|
||||
class MockLLM:
|
||||
def chat(self, prompt, **kwargs):
|
||||
# Extract the actual output to validate from the prompt
|
||||
# The LLMGuardrail sends a structured prompt with "Output to Validate:" section
|
||||
if "Output to Validate:" in prompt:
|
||||
# Split by "Output to Validate:" and get the content after it
|
||||
parts = prompt.split("Output to Validate:")
|
||||
if len(parts) > 1:
|
||||
output_content = parts[1].strip()
|
||||
# Check only the output content, not the validation criteria
|
||||
if "error" in output_content.lower():
|
||||
return "FAIL: The output contains error messages"
|
||||
return "PASS"
|
||||
|
||||
# Fallback: if no "Output to Validate:" section, check the prompt directly
|
||||
# This should rarely happen with proper LLMGuardrail usage
|
||||
if "error" in prompt.lower() and "check if" not in prompt.lower():
|
||||
return "FAIL: The output contains error messages"
|
||||
return "PASS"
|
||||
|
||||
# Create agent with mock LLM
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
role="Tester",
|
||||
goal="Test guardrails",
|
||||
backstory="I am testing the guardrail functionality"
|
||||
)
|
||||
agent.llm = MockLLM()
|
||||
|
||||
task = Task(
|
||||
description="Write a simple hello message",
|
||||
expected_output="A friendly greeting message",
|
||||
agent=agent,
|
||||
guardrail="Check if the output is professional and does not contain errors",
|
||||
max_retries=2
|
||||
)
|
||||
|
||||
# Test with good output
|
||||
good_output = TaskOutput(
|
||||
description="Test task",
|
||||
raw="Hello! This is a professional greeting message.",
|
||||
agent="Test Agent"
|
||||
)
|
||||
|
||||
result = task._process_guardrail(good_output)
|
||||
assert result.success, f"Good output should pass: {result.error}"
|
||||
print("✓ Good output passed LLM guardrail")
|
||||
|
||||
# Test with bad output
|
||||
bad_output = TaskOutput(
|
||||
description="Test task",
|
||||
raw="There was an error in the system",
|
||||
agent="Test Agent"
|
||||
)
|
||||
|
||||
result = task._process_guardrail(bad_output)
|
||||
assert not result.success, "Bad output should fail LLM guardrail"
|
||||
print("✓ Bad output failed LLM guardrail as expected")
|
||||
|
||||
print("String-based LLM guardrail test passed!\n")
|
||||
|
||||
|
||||
def test_guardrail_result():
|
||||
"""Test GuardrailResult helper methods."""
|
||||
print("Testing GuardrailResult...")
|
||||
|
||||
# Test success case
|
||||
success_result = GuardrailResult.from_tuple((True, "Modified output"))
|
||||
assert success_result.success
|
||||
assert success_result.result == "Modified output"
|
||||
assert success_result.error == ""
|
||||
print("✓ Success result created correctly")
|
||||
|
||||
# Test failure case
|
||||
failure_result = GuardrailResult.from_tuple((False, "Validation failed"))
|
||||
assert not failure_result.success
|
||||
assert failure_result.result is None
|
||||
assert failure_result.error == "Validation failed"
|
||||
print("✓ Failure result created correctly")
|
||||
|
||||
print("GuardrailResult test passed!\n")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all tests."""
|
||||
print("Running PraisonAI Agents Guardrails Tests...\n")
|
||||
|
||||
try:
|
||||
test_guardrail_result()
|
||||
test_function_guardrail()
|
||||
test_string_guardrail()
|
||||
|
||||
print("🎉 All guardrail tests passed!")
|
||||
print("\nGuardrails implementation is working correctly!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Set up basic logging
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example demonstrating guardrails functionality in PraisonAI Agents.
|
||||
|
||||
This example shows both function-based and LLM-based guardrails
|
||||
for validating task outputs.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from typing import Tuple, Any
|
||||
|
||||
# Add project to path
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
from praisonaiagents import Agent, Task, TaskOutput
|
||||
|
||||
|
||||
def email_validator(task_output: TaskOutput) -> Tuple[bool, Any]:
|
||||
"""
|
||||
Function-based guardrail to validate email content.
|
||||
|
||||
Args:
|
||||
task_output: The task output to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (success, result_or_error)
|
||||
"""
|
||||
content = task_output.raw.lower()
|
||||
|
||||
# Check for required email components
|
||||
if "subject:" not in content:
|
||||
return False, "Email must include a subject line"
|
||||
|
||||
if "dear" not in content and "hello" not in content:
|
||||
return False, "Email must include a proper greeting"
|
||||
|
||||
if len(content) < 50:
|
||||
return False, "Email content is too short"
|
||||
|
||||
if "error" in content or "problem" in content:
|
||||
return False, "Email should not mention errors or problems"
|
||||
|
||||
return True, task_output
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the guardrails example."""
|
||||
print("PraisonAI Agents - Guardrails Example")
|
||||
print("=====================================\n")
|
||||
|
||||
# Create an agent
|
||||
agent = Agent(
|
||||
name="Email Assistant",
|
||||
role="Professional Email Writer",
|
||||
goal="Write clear, professional emails",
|
||||
backstory="I am an AI assistant specialized in writing professional emails"
|
||||
)
|
||||
|
||||
print("1. Testing Function-based Guardrail")
|
||||
print("------------------------------------")
|
||||
|
||||
# Create task with function-based guardrail
|
||||
task_with_function_guardrail = Task(
|
||||
description="Write a professional email to a client about project completion",
|
||||
expected_output="A well-formatted professional email",
|
||||
agent=agent,
|
||||
guardrail=email_validator, # Function-based guardrail
|
||||
max_retries=2
|
||||
)
|
||||
|
||||
print(f"Task created with function guardrail: {email_validator.__name__}")
|
||||
print(f"Max retries: {task_with_function_guardrail.max_retries}")
|
||||
|
||||
# Simulate a task output that should pass
|
||||
good_output = TaskOutput(
|
||||
description="Email task",
|
||||
raw="""Subject: Project Completion Update
|
||||
|
||||
Dear Client,
|
||||
|
||||
I am pleased to inform you that your project has been completed successfully.
|
||||
All deliverables have been reviewed and are ready for your review.
|
||||
Please let me know if you have any questions.
|
||||
|
||||
Best regards,
|
||||
Project Team""",
|
||||
agent="Email Assistant"
|
||||
)
|
||||
|
||||
result = task_with_function_guardrail._process_guardrail(good_output)
|
||||
print(f"Good email result: {'PASSED' if result.success else 'FAILED'}")
|
||||
if not result.success:
|
||||
print(f"Error: {result.error}")
|
||||
|
||||
# Simulate a task output that should fail
|
||||
bad_output = TaskOutput(
|
||||
description="Email task",
|
||||
raw="Hi there, there was an error with your project.",
|
||||
agent="Email Assistant"
|
||||
)
|
||||
|
||||
result = task_with_function_guardrail._process_guardrail(bad_output)
|
||||
print(f"Bad email result: {'PASSED' if result.success else 'FAILED'}")
|
||||
if not result.success:
|
||||
print(f"Error: {result.error}")
|
||||
|
||||
print("\n2. Testing String-based LLM Guardrail")
|
||||
print("-------------------------------------")
|
||||
|
||||
# Create task with string-based guardrail
|
||||
task_with_llm_guardrail = Task(
|
||||
description="Write a marketing email for a new product launch",
|
||||
expected_output="Engaging marketing content",
|
||||
agent=agent,
|
||||
guardrail="Ensure the content is professional, engaging, includes a clear call-to-action, and is free of errors",
|
||||
max_retries=3
|
||||
)
|
||||
|
||||
print("Task created with LLM-based guardrail")
|
||||
print("Guardrail description: 'Ensure the content is professional, engaging, includes a clear call-to-action, and is free of errors'")
|
||||
print(f"Max retries: {task_with_llm_guardrail.max_retries}")
|
||||
|
||||
print("\n3. Backward Compatibility")
|
||||
print("-------------------------")
|
||||
|
||||
# Create task without guardrail (backward compatible)
|
||||
task_without_guardrail = Task(
|
||||
description="Write a simple thank you note",
|
||||
expected_output="A brief thank you message",
|
||||
agent=agent
|
||||
)
|
||||
|
||||
print("Task created without guardrail (backward compatible)")
|
||||
print(f"Guardrail function: {task_without_guardrail._guardrail_fn}")
|
||||
|
||||
# Test that it doesn't break existing functionality
|
||||
simple_output = TaskOutput(
|
||||
description="Thank you task",
|
||||
raw="Thank you for your business!",
|
||||
agent="Email Assistant"
|
||||
)
|
||||
|
||||
result = task_without_guardrail._process_guardrail(simple_output)
|
||||
print(f"No guardrail result: {'PASSED' if result.success else 'FAILED'}")
|
||||
|
||||
print("\n✅ Guardrails example completed successfully!")
|
||||
print("\nKey Features Demonstrated:")
|
||||
print("- Function-based guardrails with custom validation logic")
|
||||
print("- String-based LLM guardrails using natural language")
|
||||
print("- Configurable retry mechanism")
|
||||
print("- Backward compatibility with existing tasks")
|
||||
print("- Integration with TaskOutput validation")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Generated
+2054
-2052
File diff suppressed because it is too large.
Load diff
@@ -191,3 +191,17 @@ const agent = new Agent({
|
||||
// Start the agent with a prompt that will trigger tool usage
|
||||
agent.start("What's the weather in Paris, the time in Tokyo, and what is 25 * 4?");
|
||||
```
|
||||
|
||||
## MCP SSE Tool Integration
|
||||
|
||||
PraisonAI can use remote tools exposed via the Model Context Protocol over Server-Sent Events.
|
||||
|
||||
1. Start the Python SSE server in another terminal:
|
||||
```bash
|
||||
python ../../praisonai-agents/tests/mcp-sse-direct-server.py --host 127.0.0.1 --port 8080
|
||||
```
|
||||
|
||||
2. Run the TypeScript example which connects to this server:
|
||||
```bash
|
||||
npx ts-node examples/tools/mcp-sse.ts
|
||||
```
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Agent } from '../../src/agent';
|
||||
import { MCP } from '../../src/tools/mcpSse';
|
||||
|
||||
async function main() {
|
||||
// Connect to the running SSE server
|
||||
const mcp = new MCP('http://127.0.0.1:8080/sse');
|
||||
try {
|
||||
await mcp.initialize();
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to MCP SSE server:', error);
|
||||
console.error('Please ensure the server is running at http://127.0.0.1:8080/sse');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create tool functions that call the remote MCP tools
|
||||
const toolFunctions: Record<string, (...args: any[]) => Promise<any>> = {};
|
||||
|
||||
if (mcp.tools.length === 0) {
|
||||
console.warn('Warning: No MCP tools available. Make sure the MCP server is running.');
|
||||
}
|
||||
|
||||
for (const tool of mcp) {
|
||||
if (!tool || typeof tool.name !== 'string') {
|
||||
console.warn('Skipping invalid tool:', tool);
|
||||
continue;
|
||||
}
|
||||
|
||||
const paramNames = Object.keys(tool.schemaProperties || {});
|
||||
toolFunctions[tool.name] = async (...args: any[]) => {
|
||||
const params: Record<string, any> = {};
|
||||
|
||||
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
|
||||
// If single object argument, use it directly as params
|
||||
Object.assign(params, args[0]);
|
||||
} else {
|
||||
// Map positional arguments with validation
|
||||
if (args.length > paramNames.length) {
|
||||
console.warn(
|
||||
`Tool ${tool.name}: Too many arguments provided. Expected ${paramNames.length}, got ${args.length}`
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < Math.min(args.length, paramNames.length); i++) {
|
||||
params[paramNames[i]] = args[i];
|
||||
}
|
||||
}
|
||||
return tool.execute(params);
|
||||
};
|
||||
}
|
||||
|
||||
const agent = new Agent({
|
||||
instructions: 'Use the tools to greet people and report the weather.',
|
||||
name: 'MCPAgent',
|
||||
tools: mcp.toOpenAITools(),
|
||||
toolFunctions
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await agent.start('Say hello to John and tell the weather in London.');
|
||||
console.log('\nFinal Result:', result);
|
||||
} catch (error) {
|
||||
console.error('Agent execution failed:', error);
|
||||
} finally {
|
||||
// Clean up MCP connection
|
||||
await mcp.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
@@ -62,7 +62,8 @@
|
||||
"fast-xml-parser": "^4.5.1",
|
||||
"node-fetch": "^2.6.9",
|
||||
"openai": "^4.81.0",
|
||||
"praisonai": "^1.0.19"
|
||||
"praisonai": "^1.0.19",
|
||||
"@modelcontextprotocol/sdk": "^1.12.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"boxen": "^7.1.1",
|
||||
|
||||
Generated
+4862
File diff suppressed because it is too large.
Load diff
@@ -20,4 +20,5 @@ export class BaseTool implements Tool {
|
||||
}
|
||||
|
||||
// Export all tool modules
|
||||
export * from './arxivTools';
|
||||
export * from './arxivTools';
|
||||
export * from './mcpSse';
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import { BaseTool } from './index';
|
||||
|
||||
export interface MCPToolInfo {
|
||||
name: string;
|
||||
description?: string;
|
||||
inputSchema?: any;
|
||||
}
|
||||
|
||||
export class MCPTool extends BaseTool {
|
||||
private client: Client;
|
||||
private inputSchema: any;
|
||||
|
||||
constructor(info: MCPToolInfo, client: Client) {
|
||||
super(info.name, info.description || `Call the ${info.name} tool`);
|
||||
this.client = client;
|
||||
this.inputSchema = info.inputSchema || { type: 'object', properties: {}, required: [] };
|
||||
}
|
||||
|
||||
get schemaProperties(): Record<string, any> | undefined {
|
||||
return this.inputSchema?.properties;
|
||||
}
|
||||
|
||||
async execute(args: any = {}): Promise<any> {
|
||||
try {
|
||||
const result: any = await this.client.callTool({ name: this.name, arguments: args });
|
||||
if (result.structuredContent) {
|
||||
return result.structuredContent;
|
||||
}
|
||||
if (Array.isArray(result.content) && result.content.length > 0) {
|
||||
const item = result.content[0];
|
||||
if (typeof item.text === 'string') return item.text;
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to execute tool ${this.name}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
toOpenAITool() {
|
||||
return {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
parameters: this.inputSchema
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class MCP implements Iterable<MCPTool> {
|
||||
tools: MCPTool[] = [];
|
||||
private client: Client | null = null;
|
||||
|
||||
constructor(private url: string, private debug = false) {
|
||||
if (debug) {
|
||||
console.log(`MCP client initialized for URL: ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
if (this.client) {
|
||||
if (this.debug) console.log('MCP client already initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.client = new Client({ name: 'praisonai-ts-mcp', version: '1.0.0' });
|
||||
const transport = new SSEClientTransport(new URL(this.url));
|
||||
await this.client.connect(transport);
|
||||
const { tools } = await this.client.listTools();
|
||||
this.tools = tools.map((t: any) => new MCPTool({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
inputSchema: t.inputSchema
|
||||
}, this.client as Client));
|
||||
|
||||
if (this.debug) console.log(`Initialized MCP with ${this.tools.length} tools`);
|
||||
} catch (error) {
|
||||
if (this.client) {
|
||||
await this.client.close().catch(() => {});
|
||||
this.client = null;
|
||||
}
|
||||
throw new Error(`Failed to initialize MCP client: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.iterator](): Iterator<MCPTool> {
|
||||
return this.tools[Symbol.iterator]();
|
||||
}
|
||||
|
||||
toOpenAITools() {
|
||||
return this.tools.map(t => t.toOpenAITool());
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.client) {
|
||||
try {
|
||||
await this.client.close();
|
||||
} catch (error) {
|
||||
if (this.debug) {
|
||||
console.warn('Error closing MCP client:', error);
|
||||
}
|
||||
} finally {
|
||||
this.client = null;
|
||||
this.tools = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get isConnected(): boolean {
|
||||
return this.client !== null;
|
||||
}
|
||||
}
|
||||
@@ -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.2.26.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.26.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.27.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.27.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -56,7 +56,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.2.26 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.2.27 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "PraisonAI"
|
||||
version = "2.2.26"
|
||||
version = "2.2.27"
|
||||
description = "PraisonAI is an AI Agents Framework with Self Reflection. PraisonAI application combines PraisonAI Agents, AutoGen, and CrewAI into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customisation, and efficient human-agent collaboration."
|
||||
readme = "README.md"
|
||||
license = ""
|
||||
@@ -95,7 +95,7 @@ autogen = ["pyautogen>=0.2.19", "praisonai-tools>=0.0.15", "crewai"]
|
||||
|
||||
[tool.poetry]
|
||||
name = "PraisonAI"
|
||||
version = "2.2.26"
|
||||
version = "2.2.27"
|
||||
description = "PraisonAI is an AI Agents Framework with Self Reflection. PraisonAI application combines PraisonAI Agents, AutoGen, and CrewAI into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customisation, and efficient human-agent collaboration."
|
||||
authors = ["Mervin Praison"]
|
||||
license = ""
|
||||
|
||||
Generated
+1
-1
@@ -3932,7 +3932,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonai"
|
||||
version = "2.2.26"
|
||||
version = "2.2.27"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "instructor" },
|
||||
|
||||
Reference in new issue
Block a user