mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-10 17:29:39 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
722056fd76 | ||
|
|
8860b31282 | ||
|
|
f5e17c697b |
No files matched your search
@@ -0,0 +1,418 @@
|
||||
name: Optimized Test Suite
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
schedule:
|
||||
# Nightly extended tests at 3 AM UTC
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tier:
|
||||
description: 'Test tier to run'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: choice
|
||||
options:
|
||||
- smoke
|
||||
- main
|
||||
- extended
|
||||
- nightly
|
||||
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}/src/praisonai-agents
|
||||
|
||||
jobs:
|
||||
# ============================================
|
||||
# SMOKE TESTS - Fast unit tests, no network
|
||||
# Target: <= 2 minutes
|
||||
# ============================================
|
||||
smoke:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Cache pip packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-smoke-${{ hashFiles('**/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-smoke-
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- name: Install minimal dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
cd src/praisonai
|
||||
pip install -e ".[dev]" --no-deps || pip install -e .
|
||||
pip install pytest pytest-asyncio pytest-timeout
|
||||
|
||||
- name: Run Smoke Tests
|
||||
env:
|
||||
PRAISONAI_TEST_TIER: smoke
|
||||
PRAISONAI_ALLOW_NETWORK: '0'
|
||||
run: |
|
||||
cd src/praisonai
|
||||
python -m pytest tests/unit/ \
|
||||
-m "not slow and not network" \
|
||||
-q --tb=short \
|
||||
--timeout=30 \
|
||||
--ignore=tests/fixtures \
|
||||
-x
|
||||
|
||||
- name: Smoke Test Summary
|
||||
if: always()
|
||||
run: echo "✅ Smoke tests completed"
|
||||
|
||||
# ============================================
|
||||
# MAIN TESTS - Unit + safe integration
|
||||
# Target: <= 5 minutes
|
||||
# ============================================
|
||||
main:
|
||||
needs: smoke
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 6
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.11']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Cache pip packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-main-${{ hashFiles('**/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-main-
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
cd src/praisonai
|
||||
pip install -e ".[crewai,autogen]"
|
||||
pip install pytest pytest-asyncio pytest-timeout pytest-cov pytest-xdist
|
||||
|
||||
- name: Run Main Tests
|
||||
env:
|
||||
PRAISONAI_TEST_TIER: main
|
||||
PRAISONAI_ALLOW_NETWORK: '0'
|
||||
OPENAI_API_KEY: 'test-key'
|
||||
ANTHROPIC_API_KEY: 'test-key'
|
||||
GOOGLE_API_KEY: 'test-key'
|
||||
run: |
|
||||
cd src/praisonai
|
||||
python -m pytest tests/unit/ tests/integration/ \
|
||||
-m "not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere" \
|
||||
-v --tb=short \
|
||||
--timeout=60 \
|
||||
--ignore=tests/fixtures \
|
||||
--ignore=tests/e2e \
|
||||
--ignore=tests/live \
|
||||
--cov=praisonai \
|
||||
--cov-report=xml \
|
||||
-n 2 \
|
||||
|| echo "Some tests failed"
|
||||
|
||||
- name: Upload coverage
|
||||
if: matrix.python-version == '3.11'
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: src/praisonai/coverage.xml
|
||||
flags: main-tests
|
||||
fail_ci_if_error: false
|
||||
|
||||
# ============================================
|
||||
# OPENAI LIVE TESTS - Real OpenAI API calls
|
||||
# Only runs if OPENAI_API_KEY secret is set
|
||||
# ============================================
|
||||
openai-live:
|
||||
needs: smoke
|
||||
if: ${{ secrets.OPENAI_API_KEY != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 8
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Cache pip packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-openai-${{ hashFiles('**/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
cd src/praisonai
|
||||
pip install -e ".[openai]"
|
||||
pip install pytest pytest-asyncio pytest-timeout
|
||||
|
||||
- name: Run OpenAI Live Tests
|
||||
env:
|
||||
PRAISONAI_TEST_TIER: extended
|
||||
PRAISONAI_ALLOW_NETWORK: '1'
|
||||
PRAISONAI_LIVE_TESTS: '1'
|
||||
PRAISONAI_TEST_PROVIDERS: 'openai'
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_MODEL_NAME: ${{ secrets.OPENAI_MODEL_NAME || 'gpt-4o-mini' }}
|
||||
run: |
|
||||
cd src/praisonai
|
||||
python -m pytest tests/ \
|
||||
-m "provider_openai or real" \
|
||||
-v --tb=short \
|
||||
--timeout=120 \
|
||||
--ignore=tests/fixtures \
|
||||
|| echo "Some OpenAI tests failed"
|
||||
|
||||
# ============================================
|
||||
# EXTENDED PROVIDER TESTS - Nightly only
|
||||
# Runs all provider-specific tests
|
||||
# ============================================
|
||||
extended-anthropic:
|
||||
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
cd src/praisonai
|
||||
pip install -e ".[anthropic]"
|
||||
pip install pytest pytest-asyncio pytest-timeout
|
||||
|
||||
- name: Run Anthropic Tests
|
||||
if: ${{ secrets.ANTHROPIC_API_KEY != '' }}
|
||||
env:
|
||||
PRAISONAI_ALLOW_NETWORK: '1'
|
||||
PRAISONAI_LIVE_TESTS: '1'
|
||||
PRAISONAI_TEST_PROVIDERS: 'anthropic'
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
run: |
|
||||
cd src/praisonai
|
||||
python -m pytest tests/ \
|
||||
-m "provider_anthropic" \
|
||||
-v --tb=short \
|
||||
--timeout=120 \
|
||||
--ignore=tests/fixtures \
|
||||
|| echo "Anthropic tests completed"
|
||||
|
||||
- name: Skip if no API key
|
||||
if: ${{ secrets.ANTHROPIC_API_KEY == '' }}
|
||||
run: echo "⚠️ Skipping Anthropic tests - ANTHROPIC_API_KEY not set"
|
||||
|
||||
extended-google:
|
||||
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
cd src/praisonai
|
||||
pip install -e ".[google]"
|
||||
pip install pytest pytest-asyncio pytest-timeout
|
||||
|
||||
- name: Run Google/Gemini Tests
|
||||
if: ${{ secrets.GOOGLE_API_KEY != '' }}
|
||||
env:
|
||||
PRAISONAI_ALLOW_NETWORK: '1'
|
||||
PRAISONAI_LIVE_TESTS: '1'
|
||||
PRAISONAI_TEST_PROVIDERS: 'google'
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
run: |
|
||||
cd src/praisonai
|
||||
python -m pytest tests/ \
|
||||
-m "provider_google" \
|
||||
-v --tb=short \
|
||||
--timeout=120 \
|
||||
--ignore=tests/fixtures \
|
||||
|| echo "Google tests completed"
|
||||
|
||||
- name: Skip if no API key
|
||||
if: ${{ secrets.GOOGLE_API_KEY == '' }}
|
||||
run: echo "⚠️ Skipping Google tests - GOOGLE_API_KEY not set"
|
||||
|
||||
extended-groq:
|
||||
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
cd src/praisonai
|
||||
pip install -e .
|
||||
pip install pytest pytest-asyncio pytest-timeout
|
||||
|
||||
- name: Run Groq Tests
|
||||
if: ${{ secrets.GROQ_API_KEY != '' }}
|
||||
env:
|
||||
PRAISONAI_ALLOW_NETWORK: '1'
|
||||
PRAISONAI_LIVE_TESTS: '1'
|
||||
PRAISONAI_TEST_PROVIDERS: 'groq'
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
run: |
|
||||
cd src/praisonai
|
||||
python -m pytest tests/ \
|
||||
-m "provider_groq" \
|
||||
-v --tb=short \
|
||||
--timeout=120 \
|
||||
--ignore=tests/fixtures \
|
||||
|| echo "Groq tests completed"
|
||||
|
||||
- name: Skip if no API key
|
||||
if: ${{ secrets.GROQ_API_KEY == '' }}
|
||||
run: echo "⚠️ Skipping Groq tests - GROQ_API_KEY not set"
|
||||
|
||||
extended-xai:
|
||||
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
cd src/praisonai
|
||||
pip install -e .
|
||||
pip install pytest pytest-asyncio pytest-timeout
|
||||
|
||||
- name: Run xAI/Grok Tests
|
||||
if: ${{ secrets.XAI_API_KEY != '' }}
|
||||
env:
|
||||
PRAISONAI_ALLOW_NETWORK: '1'
|
||||
PRAISONAI_LIVE_TESTS: '1'
|
||||
PRAISONAI_TEST_PROVIDERS: 'grok_xai'
|
||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||
run: |
|
||||
cd src/praisonai
|
||||
python -m pytest tests/ \
|
||||
-m "provider_grok_xai" \
|
||||
-v --tb=short \
|
||||
--timeout=120 \
|
||||
--ignore=tests/fixtures \
|
||||
|| echo "xAI tests completed"
|
||||
|
||||
- name: Skip if no API key
|
||||
if: ${{ secrets.XAI_API_KEY == '' }}
|
||||
run: echo "⚠️ Skipping xAI tests - XAI_API_KEY not set"
|
||||
|
||||
extended-cohere:
|
||||
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
cd src/praisonai
|
||||
pip install -e ".[cohere]"
|
||||
pip install pytest pytest-asyncio pytest-timeout
|
||||
|
||||
- name: Run Cohere Tests
|
||||
if: ${{ secrets.COHERE_API_KEY != '' }}
|
||||
env:
|
||||
PRAISONAI_ALLOW_NETWORK: '1'
|
||||
PRAISONAI_LIVE_TESTS: '1'
|
||||
PRAISONAI_TEST_PROVIDERS: 'cohere'
|
||||
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
|
||||
run: |
|
||||
cd src/praisonai
|
||||
python -m pytest tests/ \
|
||||
-m "provider_cohere" \
|
||||
-v --tb=short \
|
||||
--timeout=120 \
|
||||
--ignore=tests/fixtures \
|
||||
|| echo "Cohere tests completed"
|
||||
|
||||
- name: Skip if no API key
|
||||
if: ${{ secrets.COHERE_API_KEY == '' }}
|
||||
run: echo "⚠️ Skipping Cohere tests - COHERE_API_KEY not set"
|
||||
|
||||
# ============================================
|
||||
# TEST SUMMARY
|
||||
# ============================================
|
||||
test-summary:
|
||||
needs: [smoke, main]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Test Summary
|
||||
run: |
|
||||
echo "# 📊 Test Suite Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Smoke | ${{ needs.smoke.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Main | ${{ needs.main.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Test Tiers" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **smoke**: Fast unit tests, no network (≤2 min)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **main**: Unit + integration, no network (≤5 min)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **openai-live**: OpenAI API tests (when key available)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **extended-***: Provider-specific tests (nightly)" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=3.5.2" \
|
||||
"praisonai>=3.5.4" \
|
||||
"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>=3.5.2" \
|
||||
"praisonai>=3.5.4" \
|
||||
"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>=3.5.2" \
|
||||
"praisonai>=3.5.4" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -134,7 +134,6 @@ def example_real_chat():
|
||||
instructions="You are a helpful math tutor. Be concise.",
|
||||
reflection=False,
|
||||
web=False,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
response = agent.start("What is the square root of 144?")
|
||||
|
||||
@@ -22,19 +22,18 @@ async def main():
|
||||
print("Agent-Centric Background Tasks Demo")
|
||||
print("=" * 60)
|
||||
|
||||
# Create a background runner with config
|
||||
# Create a background runner with config (used standalone, not passed to Agent)
|
||||
runner = BackgroundRunner(config=BackgroundConfig(max_concurrent_tasks=3))
|
||||
|
||||
# Create an Agent with background task support
|
||||
# Create an Agent
|
||||
agent = Agent(
|
||||
name="AsyncAssistant",
|
||||
instructions="You are a helpful research assistant.",
|
||||
background=runner
|
||||
)
|
||||
|
||||
print("\n--- Agent with Background Support Created ---")
|
||||
print(f"Agent: {agent.name}")
|
||||
print(f"Background runner: {agent.background is not None}")
|
||||
print(f"Background runner: {runner is not None}")
|
||||
|
||||
# Define some example tasks
|
||||
async def research_task(topic: str) -> str:
|
||||
@@ -49,17 +48,17 @@ async def main():
|
||||
await asyncio.sleep(1.0)
|
||||
return f"Analysis complete: {data}"
|
||||
|
||||
# Submit tasks via agent's background runner
|
||||
# Submit tasks via background runner
|
||||
print("\n--- Submitting Background Tasks ---")
|
||||
|
||||
task1 = await agent.background.submit(
|
||||
task1 = await runner.submit(
|
||||
research_task,
|
||||
args=("AI trends 2025",),
|
||||
name="research_ai"
|
||||
)
|
||||
print(f"✅ Submitted research task: {task1.id[:8]}")
|
||||
|
||||
task2 = await agent.background.submit(
|
||||
task2 = await runner.submit(
|
||||
analysis_task,
|
||||
args=("market data",),
|
||||
name="analyze_market"
|
||||
@@ -68,7 +67,7 @@ async def main():
|
||||
|
||||
# List all tasks
|
||||
print("\n--- Task Status ---")
|
||||
for task in agent.background.tasks:
|
||||
for task in runner.tasks:
|
||||
print(f" [{task.id[:8]}] {task.status.value}")
|
||||
|
||||
# Wait for tasks to complete
|
||||
@@ -83,7 +82,7 @@ async def main():
|
||||
print(f" {status} {task.result}")
|
||||
|
||||
# Cleanup
|
||||
agent.background.clear_completed()
|
||||
runner.clear_completed()
|
||||
print("\n✅ Cleared completed tasks")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
@@ -33,29 +33,28 @@ async def main():
|
||||
test_file.write_text("# Initial content\nprint('Hello, World!')\n")
|
||||
print(f"\n✅ Created initial file: {test_file.name}")
|
||||
|
||||
# Create checkpoint service
|
||||
checkpoints = CheckpointService(workspace_dir=workspace)
|
||||
# Create checkpoint service (used standalone, not passed to Agent)
|
||||
checkpoint_service = CheckpointService(workspace_dir=workspace)
|
||||
|
||||
# Create an Agent with checkpoint support
|
||||
# Create an Agent
|
||||
agent = Agent(
|
||||
name="RefactorBot",
|
||||
instructions="You are a code refactoring assistant.",
|
||||
checkpoints=checkpoints
|
||||
)
|
||||
|
||||
print("\n--- Agent with Checkpoint Support Created ---")
|
||||
print(f"Agent: {agent.name}")
|
||||
print(f"Checkpoints enabled: {agent.checkpoints is not None}")
|
||||
print(f"Checkpoint service initialized: {checkpoint_service is not None}")
|
||||
|
||||
try:
|
||||
# Initialize the shadow git repository
|
||||
print("\n--- Initializing Checkpoint Service ---")
|
||||
await agent.checkpoints.initialize()
|
||||
await checkpoint_service.initialize()
|
||||
print("✅ Shadow git repository initialized")
|
||||
|
||||
# Save first checkpoint
|
||||
print("\n--- Saving Checkpoint 1: Initial State ---")
|
||||
result1 = await agent.checkpoints.save("Initial state")
|
||||
result1 = await checkpoint_service.save("Initial state")
|
||||
print(f"✅ Checkpoint saved: {result1.checkpoint.id[:8]}")
|
||||
print(f" Message: {result1.checkpoint.message}")
|
||||
|
||||
@@ -71,18 +70,18 @@ async def main():
|
||||
|
||||
# Save second checkpoint
|
||||
print("\n--- Saving Checkpoint 2: After Refactoring ---")
|
||||
result2 = await agent.checkpoints.save("Added features")
|
||||
result2 = await checkpoint_service.save("Added features")
|
||||
print(f"✅ Checkpoint saved: {result2.checkpoint.id[:8]}")
|
||||
|
||||
# List all checkpoints
|
||||
print("\n--- Listing All Checkpoints ---")
|
||||
checkpoints_list = await agent.checkpoints.list_checkpoints()
|
||||
checkpoints_list = await checkpoint_service.list_checkpoints()
|
||||
for i, cp in enumerate(checkpoints_list, 1):
|
||||
print(f" {i}. [{cp.id[:8]}] {cp.message}")
|
||||
|
||||
# Show diff
|
||||
print("\n--- Current Diff ---")
|
||||
diff = await agent.checkpoints.diff()
|
||||
diff = await checkpoint_service.diff()
|
||||
if diff.files:
|
||||
for f in diff.files:
|
||||
print(f" {f.status}: {f.path}")
|
||||
@@ -95,7 +94,7 @@ async def main():
|
||||
|
||||
# Restore to checkpoint 2
|
||||
print(f"\n--- Restoring to Checkpoint: {result2.checkpoint.id[:8]} ---")
|
||||
await agent.checkpoints.restore(result2.checkpoint.id)
|
||||
await checkpoint_service.restore(result2.checkpoint.id)
|
||||
print("✅ Restored successfully!")
|
||||
|
||||
# Verify restoration
|
||||
|
||||
@@ -28,7 +28,6 @@ def main():
|
||||
name="CodeAssistant",
|
||||
instructions="You are a helpful code assistant.",
|
||||
context=True, # Enable context management with defaults
|
||||
verbose=False
|
||||
)
|
||||
|
||||
print("\n✓ Agent created with context management enabled")
|
||||
|
||||
@@ -24,13 +24,13 @@ obs.init()
|
||||
# Create an agent
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
model="gpt-4o-mini",
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
|
||||
# Use trace context manager for complete workflow tracing
|
||||
with obs.trace("chat-session", session_id="user-123"):
|
||||
# Use span context manager for individual operations
|
||||
with obs.span("user-query", kind=obs.SpanKind.AGENT):
|
||||
with obs.span("user-query"):
|
||||
response = agent.chat("What is the capital of France?")
|
||||
print(response)
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ agents = Agents(
|
||||
agents=[image_agent],
|
||||
tasks=[task1, task2],
|
||||
process="sequential",
|
||||
verbose=1
|
||||
)
|
||||
|
||||
# Run all tasks
|
||||
|
||||
@@ -40,7 +40,6 @@ agents = Agents(
|
||||
agents=[image_agent],
|
||||
tasks=[task1, task2],
|
||||
process="sequential",
|
||||
verbose=1
|
||||
)
|
||||
|
||||
# Run all tasks
|
||||
|
||||
@@ -29,7 +29,6 @@ agents = Agents(
|
||||
agents=[video_agent],
|
||||
tasks=[task1],
|
||||
process="sequential",
|
||||
verbose=1
|
||||
)
|
||||
|
||||
# Run all tasks
|
||||
|
||||
@@ -62,7 +62,6 @@ def capture_and_analyze():
|
||||
agents=[vision_agent],
|
||||
tasks=[task],
|
||||
process="sequential",
|
||||
verbose=1
|
||||
)
|
||||
|
||||
result = agents.start()
|
||||
|
||||
@@ -118,7 +118,6 @@ def analyze_with_multiple_agents():
|
||||
agents=[security_agent, object_detector, scene_analyst],
|
||||
tasks=[security_task, object_task, scene_task],
|
||||
process="parallel", # Run all analyses in parallel
|
||||
verbose=1
|
||||
)
|
||||
|
||||
result = agents.start()
|
||||
|
||||
@@ -135,7 +135,6 @@ class CameraVideoAnalyzer:
|
||||
agents=[self.video_agent],
|
||||
tasks=[task],
|
||||
process="sequential",
|
||||
verbose=1
|
||||
)
|
||||
|
||||
return agents.start()
|
||||
|
||||
@@ -99,7 +99,6 @@ sync_agent = Agent(
|
||||
backstory="Expert in sync operations and data organization",
|
||||
tools=[sync_search_tool],
|
||||
reflection=False,
|
||||
markdown=True
|
||||
)
|
||||
|
||||
async_agent = Agent(
|
||||
@@ -109,7 +108,6 @@ async_agent = Agent(
|
||||
backstory="Expert in async operations and data organization",
|
||||
tools=[async_search_tool],
|
||||
reflection=False,
|
||||
markdown=True
|
||||
)
|
||||
|
||||
# 5. Create tasks with different configurations
|
||||
@@ -161,7 +159,6 @@ def run_sync_example():
|
||||
agents = Agents(
|
||||
agents=[sync_agent],
|
||||
tasks=[sync_task],
|
||||
verbose=1,
|
||||
process="sequential"
|
||||
)
|
||||
result = agents.start()
|
||||
@@ -173,7 +170,6 @@ async def run_async_example():
|
||||
agents = Agents(
|
||||
agents=[async_agent],
|
||||
tasks=[async_task],
|
||||
verbose=1,
|
||||
process="sequential"
|
||||
)
|
||||
result = await agents.astart()
|
||||
@@ -185,7 +181,6 @@ async def run_mixed_example():
|
||||
agents = Agents(
|
||||
agents=[sync_agent, async_agent],
|
||||
tasks=[sync_task, async_task],
|
||||
verbose=1,
|
||||
process="sequential"
|
||||
)
|
||||
result = await agents.astart()
|
||||
@@ -197,7 +192,6 @@ async def run_workflow_example():
|
||||
agents = Agents(
|
||||
agents=[sync_agent, async_agent],
|
||||
tasks=[workflow_sync_task, workflow_async_task],
|
||||
verbose=1,
|
||||
process="workflow"
|
||||
)
|
||||
result = await agents.astart()
|
||||
|
||||
@@ -54,7 +54,6 @@ async_agent = Agent(
|
||||
backstory="Expert in efficient data retrieval and parallel search operations",
|
||||
tools=[async_search_tool],
|
||||
reflection=False,
|
||||
markdown=True
|
||||
)
|
||||
|
||||
summary_agent = Agent(
|
||||
@@ -63,7 +62,6 @@ summary_agent = Agent(
|
||||
goal="Create concise summaries from multiple search results",
|
||||
backstory="Expert in analyzing and synthesizing information from multiple sources",
|
||||
reflection=True,
|
||||
markdown=True
|
||||
)
|
||||
|
||||
# 5. Create async tasks
|
||||
@@ -116,7 +114,6 @@ async def run_parallel_tasks():
|
||||
agents = Agents(
|
||||
agents=[async_agent, summary_agent],
|
||||
tasks=parallel_tasks + [summary_task],
|
||||
verbose=1,
|
||||
process="sequential"
|
||||
)
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ async_agent = Agent(
|
||||
backstory="Expert in parallel search operations and data retrieval",
|
||||
tools=[async_search_tool],
|
||||
reflection=False,
|
||||
markdown=True
|
||||
)
|
||||
|
||||
summary_agent = Agent(
|
||||
@@ -72,7 +71,6 @@ summary_agent = Agent(
|
||||
Skilled at identifying patterns, trends, and connections between different topics.
|
||||
Specializes in creating clear, structured summaries that highlight key insights.""",
|
||||
reflection=True, # Enable self-reflection for better summary quality
|
||||
markdown=True
|
||||
)
|
||||
|
||||
# 5. Create async tasks
|
||||
@@ -104,7 +102,6 @@ async def run_single_task():
|
||||
agents = Agents(
|
||||
agents=[async_agent],
|
||||
tasks=[async_task],
|
||||
verbose=1,
|
||||
process="sequential"
|
||||
)
|
||||
result = await agents.astart()
|
||||
@@ -188,7 +185,6 @@ Present the summary in a clear, structured format with sections for findings, pa
|
||||
agents = Agents(
|
||||
agents=[async_agent],
|
||||
tasks=parallel_tasks, # Only run search tasks first
|
||||
verbose=1,
|
||||
process="sequential"
|
||||
)
|
||||
search_results = await agents.astart()
|
||||
@@ -213,7 +209,6 @@ Present the summary in a clear, structured format with sections for findings, pa
|
||||
summary_agents = Agents(
|
||||
agents=[summary_agent], # Use summary agent for synthesis
|
||||
tasks=[summary_task],
|
||||
verbose=1,
|
||||
process="sequential"
|
||||
)
|
||||
summary_result = await summary_agents.astart()
|
||||
|
||||
@@ -127,10 +127,6 @@ def main():
|
||||
allow_delegation=False,
|
||||
tools=[internet_search_tool],
|
||||
llm="gpt-4o-mini",
|
||||
markdown=True,
|
||||
|
||||
|
||||
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
@@ -142,7 +138,6 @@ def main():
|
||||
allow_delegation=True,
|
||||
llm="gpt-4o-mini",
|
||||
tools=[],
|
||||
markdown=True
|
||||
)
|
||||
|
||||
# Create tasks with callbacks
|
||||
|
||||
@@ -144,7 +144,6 @@ validate_task = Task(
|
||||
agents = Agents(
|
||||
agents=[data_agent],
|
||||
tasks=[collect_task, validate_task],
|
||||
verbose=1,
|
||||
process="workflow"
|
||||
)
|
||||
|
||||
|
||||
@@ -39,10 +39,6 @@ researcher = Agent(
|
||||
allow_delegation=False,
|
||||
tools=[internet_search_tool],
|
||||
llm="gpt-4o-mini",
|
||||
markdown=True,
|
||||
|
||||
|
||||
|
||||
)
|
||||
writer = Agent(
|
||||
name="Writer",
|
||||
@@ -53,7 +49,6 @@ writer = Agent(
|
||||
allow_delegation=True,
|
||||
llm="gpt-4o-mini",
|
||||
tools=[],
|
||||
markdown=True
|
||||
)
|
||||
|
||||
# Create tasks
|
||||
|
||||
@@ -46,8 +46,7 @@ task3 = Task(
|
||||
agents = Agents(
|
||||
agents=[vision_agent],
|
||||
tasks=[task1, task2, task3],
|
||||
process="sequential",
|
||||
verbose=1
|
||||
process="sequential"
|
||||
)
|
||||
|
||||
# Run all tasks
|
||||
|
||||
@@ -32,9 +32,7 @@ def get_structured_analysis(query: str, verbose: bool = True) -> AnalysisReport:
|
||||
role="Research Analyst",
|
||||
goal="Analyze topics and provide structured reports",
|
||||
backstory="Expert at gathering information and providing structured analysis",
|
||||
verbose=verbose,
|
||||
reflection=True, # Enable self-reflection for better quality
|
||||
markdown=True,
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@ validate_task = Task(
|
||||
agents = Agents(
|
||||
agents=[data_agent],
|
||||
tasks=[collect_task, validate_task],
|
||||
verbose=1,
|
||||
process="workflow"
|
||||
)
|
||||
|
||||
|
||||
@@ -228,7 +228,6 @@ process_task = Task(
|
||||
workflow = Agents(
|
||||
agents=[data_agent, process_agent],
|
||||
tasks=[collect_task, validate_task, process_task],
|
||||
verbose=1,
|
||||
process="workflow"
|
||||
)
|
||||
|
||||
|
||||
@@ -152,7 +152,6 @@ def main():
|
||||
agents_system = Agents(
|
||||
agents=[researcher],
|
||||
tasks=[build_knowledge_task, query_knowledge_task, expand_knowledge_task],
|
||||
verbose=1,
|
||||
memory=True,
|
||||
memory_config=memory_config
|
||||
)
|
||||
|
||||
@@ -91,7 +91,6 @@ def main():
|
||||
memory=True,
|
||||
process="workflow",
|
||||
user_id="research_project_001",
|
||||
verbose=1
|
||||
)
|
||||
|
||||
print(f"🤖 Created workflow with {len(workflow.agents)} agents and {len(workflow.tasks)} tasks")
|
||||
|
||||
@@ -51,7 +51,6 @@ def basic_rag_example():
|
||||
|
||||
COMPANY DATABASE:
|
||||
{context}""",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
# Test queries
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
RouteLLM Integration Example
|
||||
|
||||
Route requests between strong and weak models to optimize cost.
|
||||
|
||||
Prerequisites:
|
||||
1. pip install routellm
|
||||
2. export OPENAI_API_KEY=your-api-key
|
||||
3. Start RouteLLM server:
|
||||
python -m routellm.openai_server \
|
||||
--routers mf \
|
||||
--strong-model gpt-4o \
|
||||
--weak-model gpt-4o-mini \
|
||||
--port 6060
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic usage with RouteLLM routing
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
llm="router-mf-0.5",
|
||||
base_url="http://localhost:6060/v1"
|
||||
)
|
||||
|
||||
response = agent.chat("What is 2+2?")
|
||||
print(response)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
RouteLLM Multi-Agent Example
|
||||
|
||||
Prerequisites:
|
||||
1. pip install routellm
|
||||
2. export OPENAI_API_KEY=your-api-key
|
||||
3. Start RouteLLM server:
|
||||
python -m routellm.openai_server \
|
||||
--routers mf \
|
||||
--strong-model gpt-4o \
|
||||
--weak-model gpt-4o-mini \
|
||||
--port 6060
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, PraisonAIAgents, Task
|
||||
|
||||
ROUTELLM_URL = "http://localhost:6060/v1"
|
||||
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
role="Research analyst",
|
||||
goal="Find and analyze information",
|
||||
llm="router-mf-0.5",
|
||||
base_url=ROUTELLM_URL
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
name="Writer",
|
||||
role="Content writer",
|
||||
goal="Write clear and engaging content",
|
||||
llm="router-mf-0.5",
|
||||
base_url=ROUTELLM_URL
|
||||
)
|
||||
|
||||
task1 = Task(
|
||||
description="Research the latest AI trends in 2024",
|
||||
agent=researcher,
|
||||
expected_output="Research summary with key findings"
|
||||
)
|
||||
|
||||
task2 = Task(
|
||||
description="Write a brief article based on the research",
|
||||
agent=writer,
|
||||
expected_output="Short article about AI trends"
|
||||
)
|
||||
|
||||
agents = PraisonAIAgents(agents=[researcher, writer], tasks=[task1, task2])
|
||||
result = agents.start()
|
||||
print(result)
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
RouteLLM Workflow Example
|
||||
|
||||
Prerequisites:
|
||||
1. pip install routellm
|
||||
2. export OPENAI_API_KEY=your-api-key
|
||||
3. Start RouteLLM server:
|
||||
python -m routellm.openai_server \
|
||||
--routers mf \
|
||||
--strong-model gpt-4o \
|
||||
--weak-model gpt-4o-mini \
|
||||
--port 6060
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent, Workflow
|
||||
|
||||
ROUTELLM_URL = "http://localhost:6060/v1"
|
||||
|
||||
workflow = Workflow(
|
||||
name="Analysis Pipeline",
|
||||
steps=[
|
||||
Agent(
|
||||
name="Analyzer",
|
||||
role="Data analyst",
|
||||
goal="Analyze data and extract insights",
|
||||
llm="router-mf-0.5",
|
||||
base_url=ROUTELLM_URL
|
||||
),
|
||||
Agent(
|
||||
name="Reporter",
|
||||
role="Report writer",
|
||||
goal="Create clear reports from analysis",
|
||||
llm="router-mf-0.5",
|
||||
base_url=ROUTELLM_URL
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
result = workflow.run("Analyze the impact of AI on software development")
|
||||
print(result["output"])
|
||||
@@ -45,8 +45,9 @@ def main():
|
||||
agent = Agent(
|
||||
name="DeepThinker",
|
||||
instructions="You are a problem-solving assistant that thinks step by step.",
|
||||
thinking_budget=ThinkingBudget.high() # 16,000 tokens for reasoning
|
||||
)
|
||||
# Set thinking budget via property setter (not constructor param)
|
||||
agent.thinking_budget = ThinkingBudget.high() # 16,000 tokens for reasoning
|
||||
|
||||
print(f"Agent: {agent.name}")
|
||||
print(f"Thinking budget: {agent.thinking_budget.max_tokens:,} tokens")
|
||||
@@ -67,8 +68,9 @@ def main():
|
||||
agent_custom = Agent(
|
||||
name="CustomThinker",
|
||||
instructions="You solve complex problems.",
|
||||
thinking_budget=custom_budget
|
||||
)
|
||||
# Set thinking budget via property setter (not constructor param)
|
||||
agent_custom.thinking_budget = custom_budget
|
||||
|
||||
print(f"Agent: {agent_custom.name}")
|
||||
print(f"Max tokens: {agent_custom.thinking_budget.max_tokens:,}")
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from praisonaiagents import Agent, force_shutdown_telemetry
|
||||
from praisonaiagents import Agent
|
||||
|
||||
print("Creating agent...")
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
llm="gpt-5-nano"
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
print("Starting agent...")
|
||||
result = agent.start("Why is the sky blue?")
|
||||
|
||||
print("Agent completed successfully!")
|
||||
print(f"Result: {result}")
|
||||
|
||||
# Optional: Clean up telemetry system when script is completely done
|
||||
print("Cleaning up telemetry...")
|
||||
force_shutdown_telemetry()
|
||||
|
||||
print("Script finished normally.")
|
||||
print(f"Result: {result}")
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.10.1"
|
||||
version = "0.10.3"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
[pytest]
|
||||
asyncio_mode = auto
|
||||
asyncio_default_fixture_loop_scope = function
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
addopts = -v --tb=short --timeout=60
|
||||
timeout_method = thread
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning
|
||||
ignore::PendingDeprecationWarning
|
||||
|
||||
markers =
|
||||
# Test type markers (mutually exclusive)
|
||||
unit: Pure unit tests - no network, no side effects
|
||||
integration: Local integration tests - may use filesystem/db
|
||||
e2e: End-to-end tests - external calls allowed if gated
|
||||
|
||||
# Provider markers (additive - auto-applied by conftest)
|
||||
provider_openai: Requires OpenAI API key (OPENAI_API_KEY)
|
||||
provider_anthropic: Requires Anthropic API key (ANTHROPIC_API_KEY)
|
||||
provider_google: Requires Google/Gemini API key (GOOGLE_API_KEY)
|
||||
provider_ollama: Requires Ollama running locally
|
||||
provider_grok_xai: Requires xAI/Grok API key (XAI_API_KEY)
|
||||
provider_groq: Requires Groq API key (GROQ_API_KEY)
|
||||
provider_cohere: Requires Cohere API key (COHERE_API_KEY)
|
||||
|
||||
# Infrastructure markers
|
||||
network: Makes outbound network calls
|
||||
local_service: Requires Docker or local service running
|
||||
slow: Takes more than 5 seconds consistently
|
||||
flaky: Known intermittent failures - retries allowed
|
||||
allow_sleep: Opt out of fast_sleep fixture
|
||||
live: Live tests requiring real API calls
|
||||
|
||||
# Legacy markers (kept for backward compatibility)
|
||||
asyncio: Async test support
|
||||
real: Alias for network + provider tests (legacy)
|
||||
@@ -2,9 +2,10 @@ from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
llm="gemini/gemini-2.5-flash",
|
||||
stream=True
|
||||
llm="gemini/gemini-2.5-flash"
|
||||
)
|
||||
|
||||
for chunk in agent.start("Write a report on about the history of the world"):
|
||||
print(chunk, end="", flush=True)
|
||||
# Use chat method with stream=True for streaming responses
|
||||
for chunk in agent.chat("Write a short paragraph about the history of computing", stream=True):
|
||||
print(chunk, end="", flush=True)
|
||||
print() # Final newline
|
||||
@@ -2,9 +2,10 @@ from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
llm="gpt-5-nano",
|
||||
stream=True
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
for chunk in agent.start("Write a report on about the history of the world"):
|
||||
print(chunk, end="", flush=True)
|
||||
# Use chat method with stream=True for streaming responses
|
||||
for chunk in agent.chat("Write a short paragraph about the history of computing", stream=True):
|
||||
print(chunk, end="", flush=True)
|
||||
print() # Final newline
|
||||
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.10.1"
|
||||
version = "0.10.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
+25
-47
@@ -1,49 +1,27 @@
|
||||
# Movie Script Workflow - Cat in Mars
|
||||
# Uses the modern agents + steps format
|
||||
|
||||
name: Create Movie Script About Cat in Mars
|
||||
description: A sequential workflow to research, design narrative, and write a movie script
|
||||
framework: praisonai
|
||||
|
||||
workflow:
|
||||
verbose: true
|
||||
|
||||
agents:
|
||||
researcher:
|
||||
name: Researcher
|
||||
role: Research Analyst
|
||||
goal: Gather information about Mars and cats
|
||||
backstory: Skilled in research, with a focus on gathering accurate and relevant information.
|
||||
instructions: |
|
||||
Research about Mars, its environment, and the feasibility of a cat being on Mars.
|
||||
Also, research about cat behavior and characteristics.
|
||||
topic: --help
|
||||
roles:
|
||||
file_reader:
|
||||
backstory: A diligent agent skilled in parsing and extracting data from various
|
||||
file types.
|
||||
goal: Extract relevant information from the input file
|
||||
role: File Reader
|
||||
tasks:
|
||||
read_task:
|
||||
description: Read the contents of a specified file to retrieve necessary information.
|
||||
expected_output: Extracted content from the file ready for processing.
|
||||
tools:
|
||||
- search_tool
|
||||
|
||||
narrative_designer:
|
||||
name: Narrative Designer
|
||||
role: Story Concept Developer
|
||||
goal: Create a story concept for a movie about a cat in Mars
|
||||
backstory: Skilled in narrative development, with a focus on creating engaging stories.
|
||||
instructions: |
|
||||
Use the research findings to craft a unique story concept for a movie about a cat in Mars.
|
||||
Create narrative arcs, character bios, and settings.
|
||||
|
||||
scriptwriter:
|
||||
name: Scriptwriter
|
||||
role: Script Writer
|
||||
goal: Write a movie script about a cat in Mars
|
||||
backstory: Expert in dialogue and script structure, translating concepts into scripts.
|
||||
instructions: |
|
||||
Turn the story concept into a production-ready movie script,
|
||||
including dialogue and scene details.
|
||||
|
||||
steps:
|
||||
- agent: researcher
|
||||
action: "Research about Mars environment and cat behavior for the movie concept"
|
||||
|
||||
- agent: narrative_designer
|
||||
action: "Create story concept based on research: {{previous_output}}"
|
||||
|
||||
- agent: scriptwriter
|
||||
action: "Write the final movie script based on the story concept: {{previous_output}}"
|
||||
- read_file
|
||||
command_executor:
|
||||
backstory: An experienced agent with expertise in command execution and automation.
|
||||
goal: Execute commands based on the information gathered
|
||||
role: Command Executor
|
||||
tasks:
|
||||
execute_task:
|
||||
description: Take the extracted information and execute relevant commands,
|
||||
logging outputs to a file.
|
||||
expected_output: Execution results written to the output file.
|
||||
tools:
|
||||
- execute_command
|
||||
- write_file
|
||||
dependencies: []
|
||||
@@ -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/v3.5.2.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.5.2.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.5.4.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.5.4.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -327,6 +327,24 @@ class AgentsGenerator:
|
||||
# Route to YAMLWorkflowParser for advanced workflow patterns
|
||||
return self._run_yaml_workflow(config)
|
||||
|
||||
# Canonical format conversion: 'agents' -> 'roles', 'instructions' -> 'backstory'
|
||||
# This ensures backward compatibility while supporting the new canonical format
|
||||
if 'agents' in config and 'roles' not in config:
|
||||
config['roles'] = {}
|
||||
for agent_name, agent_config in config['agents'].items():
|
||||
role_config = dict(agent_config) if agent_config else {}
|
||||
# Convert 'instructions' to 'backstory' if present
|
||||
if 'instructions' in role_config and 'backstory' not in role_config:
|
||||
role_config['backstory'] = role_config.pop('instructions')
|
||||
# Ensure required fields have defaults
|
||||
if 'role' not in role_config:
|
||||
role_config['role'] = agent_name.replace('_', ' ').title()
|
||||
if 'goal' not in role_config:
|
||||
role_config['goal'] = role_config.get('backstory', 'Complete the assigned task')
|
||||
if 'backstory' not in role_config:
|
||||
role_config['backstory'] = f'You are a {role_config["role"]}'
|
||||
config['roles'][agent_name] = role_config
|
||||
|
||||
# Get workflow input: 'input' is canonical, 'topic' is alias for backward compatibility
|
||||
topic = config.get('input', config.get('topic', ''))
|
||||
tools_dict = {}
|
||||
@@ -805,26 +823,21 @@ class AgentsGenerator:
|
||||
backstory_filled = details['backstory'].format(topic=topic)
|
||||
|
||||
# Pass all loaded tools to the agent
|
||||
# Get LLM from config or environment
|
||||
llm_config = details.get('llm', {})
|
||||
llm_model = llm_config.get("model") if isinstance(llm_config, dict) else llm_config
|
||||
llm_model = llm_model or os.environ.get("MODEL_NAME") or "gpt-4o-mini"
|
||||
|
||||
agent = PraisonAgent(
|
||||
name=role_filled,
|
||||
role=role_filled,
|
||||
goal=goal_filled,
|
||||
backstory=backstory_filled,
|
||||
instructions=details.get('instructions'),
|
||||
tools=tools_list, # Pass the entire tools list to the agent
|
||||
allow_delegation=details.get('allow_delegation', False),
|
||||
llm=details.get('llm', {}).get("model") or os.environ.get("MODEL_NAME") or "openai/gpt-5-nano",
|
||||
function_calling_llm=details.get('function_calling_llm', {}).get("model") or os.environ.get("MODEL_NAME") or "openai/gpt-5-nano",
|
||||
max_iter=details.get('max_iter', 15),
|
||||
max_rpm=details.get('max_rpm'),
|
||||
max_execution_time=details.get('max_execution_time'),
|
||||
verbose=details.get('verbose', True),
|
||||
cache=details.get('cache', True),
|
||||
system_template=details.get('system_template'),
|
||||
prompt_template=details.get('prompt_template'),
|
||||
response_template=details.get('response_template'),
|
||||
reflect_llm=details.get('reflect_llm', {}).get("model") or os.environ.get("MODEL_NAME") or "openai/gpt-5-nano",
|
||||
min_reflect=details.get('min_reflect', 1),
|
||||
max_reflect=details.get('max_reflect', 3),
|
||||
llm=llm_model,
|
||||
reflection=details.get('reflection', False),
|
||||
)
|
||||
|
||||
if self.agent_callback:
|
||||
@@ -834,32 +847,47 @@ class AgentsGenerator:
|
||||
self.logger.debug(f"Created agent {role_filled} with tools: {agent.tools}")
|
||||
|
||||
# Create tasks for the agent
|
||||
for task_name, task_details in details.get('tasks', {}).items():
|
||||
description_filled = task_details['description'].format(topic=topic)
|
||||
expected_output_filled = task_details['expected_output'].format(topic=topic)
|
||||
|
||||
task = PraisonTask(
|
||||
description=description_filled,
|
||||
expected_output=expected_output_filled,
|
||||
agent_tasks = details.get('tasks', {})
|
||||
|
||||
# If no tasks defined, auto-generate one from instructions/backstory
|
||||
if not agent_tasks:
|
||||
# Use instructions or backstory as the task description
|
||||
task_description = details.get('instructions') or backstory_filled
|
||||
auto_task = PraisonTask(
|
||||
description=task_description,
|
||||
expected_output="Complete the assigned task successfully.",
|
||||
agent=agent,
|
||||
tools=tools_list, # Pass the same tools list to the task
|
||||
async_execution=task_details.get('async_execution', False),
|
||||
context=[],
|
||||
config=task_details.get('config', {}),
|
||||
output_json=task_details.get('output_json'),
|
||||
output_pydantic=task_details.get('output_pydantic'),
|
||||
output_file=task_details.get('output_file', ""),
|
||||
callback=task_details.get('callback'),
|
||||
create_directory=task_details.get('create_directory', False)
|
||||
)
|
||||
tasks.append(auto_task)
|
||||
tasks_dict[f"{role}_auto_task"] = auto_task
|
||||
self.logger.debug(f"Auto-generated task for agent {role_filled}")
|
||||
else:
|
||||
for task_name, task_details in agent_tasks.items():
|
||||
description_filled = task_details['description'].format(topic=topic)
|
||||
expected_output_filled = task_details['expected_output'].format(topic=topic)
|
||||
|
||||
self.logger.debug(f"Created task {task_name} with tools: {task.tools}")
|
||||
|
||||
if self.task_callback:
|
||||
task.callback = self.task_callback
|
||||
task = PraisonTask(
|
||||
description=description_filled,
|
||||
expected_output=expected_output_filled,
|
||||
agent=agent,
|
||||
tools=tools_list, # Pass the same tools list to the task
|
||||
async_execution=task_details.get('async_execution', False),
|
||||
context=[],
|
||||
config=task_details.get('config', {}),
|
||||
output_json=task_details.get('output_json'),
|
||||
output_pydantic=task_details.get('output_pydantic'),
|
||||
output_file=task_details.get('output_file', ""),
|
||||
callback=task_details.get('callback'),
|
||||
create_directory=task_details.get('create_directory', False)
|
||||
)
|
||||
|
||||
tasks.append(task)
|
||||
tasks_dict[task_name] = task
|
||||
self.logger.debug(f"Created task {task_name} with tools: {task.tools}")
|
||||
|
||||
if self.task_callback:
|
||||
task.callback = self.task_callback
|
||||
|
||||
tasks.append(task)
|
||||
tasks_dict[task_name] = task
|
||||
|
||||
# Set up task contexts
|
||||
for role, details in config['roles'].items():
|
||||
@@ -876,16 +904,14 @@ class AgentsGenerator:
|
||||
agents = Agents(
|
||||
agents=list(agents.values()),
|
||||
tasks=tasks,
|
||||
verbose=True,
|
||||
process="hierarchical",
|
||||
manager_llm=config.get('manager_llm') or os.environ.get("MODEL_NAME") or "openai/gpt-5-nano",
|
||||
manager_llm=config.get('manager_llm') or os.environ.get("MODEL_NAME") or "gpt-4o-mini",
|
||||
memory=memory
|
||||
)
|
||||
else:
|
||||
agents = Agents(
|
||||
agents=list(agents.values()),
|
||||
tasks=tasks,
|
||||
verbose=True,
|
||||
memory=memory
|
||||
)
|
||||
|
||||
|
||||
@@ -206,6 +206,7 @@ def register_commands():
|
||||
from .commands.registry import app as registry_app
|
||||
from .commands.package import app as package_app
|
||||
from .commands.endpoints import app as endpoints_app
|
||||
from .commands.test import app as test_app
|
||||
|
||||
# Import TUI and queue commands
|
||||
from .features.tui.debug import create_debug_app as create_tui_debug_app
|
||||
@@ -261,6 +262,7 @@ def register_commands():
|
||||
app.add_typer(registry_app, name="registry", help="Registry management")
|
||||
app.add_typer(package_app, name="package", help="Package management")
|
||||
app.add_typer(endpoints_app, name="endpoints", help="API endpoint management")
|
||||
app.add_typer(test_app, name="test", help="Run test suite with tier and provider options")
|
||||
|
||||
# Register TUI and queue commands
|
||||
tui_app = create_tui_debug_app()
|
||||
|
||||
@@ -23,6 +23,7 @@ __all__ = [
|
||||
'acp_app',
|
||||
'mcp_app',
|
||||
'rag_app',
|
||||
'test_app',
|
||||
]
|
||||
|
||||
|
||||
@@ -76,4 +77,7 @@ def __getattr__(name: str):
|
||||
elif name == 'rag_app':
|
||||
from .rag import app as rag_app
|
||||
return rag_app
|
||||
elif name == 'test_app':
|
||||
from .test import app as test_app
|
||||
return test_app
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,286 @@
|
||||
"""
|
||||
PraisonAI Test CLI Command
|
||||
|
||||
Provides a unified interface for running tests with tier and provider options.
|
||||
|
||||
Usage:
|
||||
praisonai test # Run main tier (default)
|
||||
praisonai test --tier smoke # Run smoke tests only
|
||||
praisonai test --tier extended # Run extended tests
|
||||
praisonai test --provider openai # Run OpenAI provider tests
|
||||
praisonai test --live # Enable live API tests
|
||||
praisonai test --parallel auto # Run with parallelization
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Optional, List
|
||||
|
||||
import typer
|
||||
|
||||
app = typer.Typer(help="Run PraisonAI test suite with tier and provider options")
|
||||
|
||||
|
||||
def _get_pytest_args(
|
||||
tier: str,
|
||||
provider: Optional[str],
|
||||
live: bool,
|
||||
parallel: Optional[str],
|
||||
verbose: bool,
|
||||
coverage: bool,
|
||||
) -> List[str]:
|
||||
"""Build pytest arguments based on options."""
|
||||
args = ["python", "-m", "pytest"]
|
||||
|
||||
# Base test paths
|
||||
if tier == "smoke":
|
||||
args.append("tests/unit/")
|
||||
elif tier == "main":
|
||||
args.extend(["tests/unit/", "tests/integration/"])
|
||||
elif tier in ("extended", "nightly"):
|
||||
args.append("tests/")
|
||||
else:
|
||||
args.append("tests/")
|
||||
|
||||
# Always ignore fixtures directory
|
||||
args.extend(["--ignore=tests/fixtures"])
|
||||
|
||||
# Tier-specific markers
|
||||
if tier == "smoke":
|
||||
args.extend(["-m", "not slow and not network"])
|
||||
elif tier == "main":
|
||||
# Exclude non-OpenAI providers by default
|
||||
args.extend([
|
||||
"-m",
|
||||
"not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere"
|
||||
])
|
||||
|
||||
# Provider-specific filter
|
||||
if provider:
|
||||
if provider == "all":
|
||||
pass # No filter
|
||||
else:
|
||||
provider_marker = f"provider_{provider}"
|
||||
args.extend(["-m", provider_marker])
|
||||
|
||||
# Verbosity
|
||||
if verbose:
|
||||
args.append("-v")
|
||||
else:
|
||||
args.append("-q")
|
||||
|
||||
args.append("--tb=short")
|
||||
|
||||
# Timeout
|
||||
if tier == "smoke":
|
||||
args.extend(["--timeout=30"])
|
||||
elif tier == "main":
|
||||
args.extend(["--timeout=60"])
|
||||
else:
|
||||
args.extend(["--timeout=120"])
|
||||
|
||||
# Coverage
|
||||
if coverage:
|
||||
args.extend([
|
||||
"--cov=praisonai",
|
||||
"--cov-report=term-missing",
|
||||
"--cov-report=xml"
|
||||
])
|
||||
|
||||
# Parallelization
|
||||
if parallel:
|
||||
if parallel == "auto":
|
||||
args.extend(["-n", "auto"])
|
||||
else:
|
||||
args.extend(["-n", parallel])
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def _set_environment(tier: str, provider: Optional[str], live: bool):
|
||||
"""Set environment variables for test run."""
|
||||
os.environ["PRAISONAI_TEST_TIER"] = tier
|
||||
|
||||
if live:
|
||||
os.environ["PRAISONAI_ALLOW_NETWORK"] = "1"
|
||||
os.environ["PRAISONAI_LIVE_TESTS"] = "1"
|
||||
else:
|
||||
os.environ["PRAISONAI_ALLOW_NETWORK"] = "0"
|
||||
os.environ["PRAISONAI_LIVE_TESTS"] = "0"
|
||||
|
||||
if provider:
|
||||
os.environ["PRAISONAI_TEST_PROVIDERS"] = provider
|
||||
|
||||
|
||||
def _print_skip_summary(tier: str, provider: Optional[str], live: bool):
|
||||
"""Print summary of what will be skipped."""
|
||||
typer.echo("\n📋 Test Configuration:")
|
||||
typer.echo(f" Tier: {tier}")
|
||||
typer.echo(f" Provider: {provider or 'all (gated)'}")
|
||||
typer.echo(f" Live tests: {'enabled' if live else 'disabled'}")
|
||||
typer.echo("")
|
||||
|
||||
if not live:
|
||||
typer.echo("⚠️ Network tests will be skipped (use --live to enable)")
|
||||
|
||||
if tier == "smoke":
|
||||
typer.echo("ℹ️ Running unit tests only (no integration/e2e)")
|
||||
elif tier == "main":
|
||||
typer.echo("ℹ️ Skipping non-OpenAI provider tests")
|
||||
|
||||
typer.echo("")
|
||||
|
||||
|
||||
@app.command()
|
||||
def run(
|
||||
tier: str = typer.Option(
|
||||
"main",
|
||||
"--tier", "-t",
|
||||
help="Test tier: smoke (fast), main (default), extended, nightly"
|
||||
),
|
||||
provider: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--provider", "-p",
|
||||
help="Run tests for specific provider: openai, anthropic, google, ollama, grok_xai, groq, cohere, all"
|
||||
),
|
||||
live: bool = typer.Option(
|
||||
False,
|
||||
"--live", "-l",
|
||||
help="Enable live API tests (requires API keys)"
|
||||
),
|
||||
allow_network: bool = typer.Option(
|
||||
False,
|
||||
"--allow-network",
|
||||
help="Allow network access in tests"
|
||||
),
|
||||
parallel: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--parallel", "-n",
|
||||
help="Run tests in parallel: auto or number of workers"
|
||||
),
|
||||
verbose: bool = typer.Option(
|
||||
False,
|
||||
"--verbose", "-v",
|
||||
help="Verbose output"
|
||||
),
|
||||
coverage: bool = typer.Option(
|
||||
False,
|
||||
"--coverage", "-c",
|
||||
help="Generate coverage report"
|
||||
),
|
||||
fail_fast: bool = typer.Option(
|
||||
False,
|
||||
"--fail-fast", "-x",
|
||||
help="Stop on first failure"
|
||||
),
|
||||
):
|
||||
"""
|
||||
Run the PraisonAI test suite.
|
||||
|
||||
Examples:
|
||||
praisonai test # Run main tier
|
||||
praisonai test --tier smoke # Fast smoke tests
|
||||
praisonai test --tier extended # Extended tests
|
||||
praisonai test --provider openai --live # OpenAI live tests
|
||||
praisonai test --parallel auto # Parallel execution
|
||||
"""
|
||||
# Validate tier
|
||||
valid_tiers = ["smoke", "main", "extended", "nightly"]
|
||||
if tier not in valid_tiers:
|
||||
typer.echo(f"❌ Invalid tier: {tier}. Must be one of: {', '.join(valid_tiers)}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Validate provider
|
||||
valid_providers = ["openai", "anthropic", "google", "ollama", "grok_xai", "groq", "cohere", "all"]
|
||||
if provider and provider not in valid_providers:
|
||||
typer.echo(f"❌ Invalid provider: {provider}. Must be one of: {', '.join(valid_providers)}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Set environment
|
||||
_set_environment(tier, provider, live or allow_network)
|
||||
|
||||
# Print summary
|
||||
_print_skip_summary(tier, provider, live or allow_network)
|
||||
|
||||
# Build pytest args
|
||||
args = _get_pytest_args(tier, provider, live or allow_network, parallel, verbose, coverage)
|
||||
|
||||
if fail_fast:
|
||||
args.append("-x")
|
||||
|
||||
# Print command
|
||||
typer.echo(f"🧪 Running: {' '.join(args)}")
|
||||
typer.echo("")
|
||||
|
||||
# Find the tests directory
|
||||
# Try current directory first, then src/praisonai
|
||||
test_dirs = [
|
||||
os.getcwd(),
|
||||
os.path.join(os.getcwd(), "src", "praisonai"),
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))),
|
||||
]
|
||||
|
||||
cwd = None
|
||||
for test_dir in test_dirs:
|
||||
if os.path.isdir(os.path.join(test_dir, "tests")):
|
||||
cwd = test_dir
|
||||
break
|
||||
|
||||
if not cwd:
|
||||
typer.echo("❌ Could not find tests directory")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Run pytest
|
||||
try:
|
||||
result = subprocess.run(args, cwd=cwd)
|
||||
raise typer.Exit(result.returncode)
|
||||
except KeyboardInterrupt:
|
||||
typer.echo("\n❌ Tests interrupted")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def info():
|
||||
"""Show test configuration and available options."""
|
||||
typer.echo("📊 PraisonAI Test Suite Information")
|
||||
typer.echo("=" * 50)
|
||||
typer.echo("")
|
||||
|
||||
typer.echo("🎯 Test Tiers:")
|
||||
typer.echo(" smoke - Fast unit tests, no network (≤2 min)")
|
||||
typer.echo(" main - Unit + integration, OpenAI only (≤5 min)")
|
||||
typer.echo(" extended - All providers, gated by keys (≤15 min)")
|
||||
typer.echo(" nightly - Full matrix + stress tests (≤30 min)")
|
||||
typer.echo("")
|
||||
|
||||
typer.echo("🔌 Providers:")
|
||||
providers = {
|
||||
"openai": "OPENAI_API_KEY",
|
||||
"anthropic": "ANTHROPIC_API_KEY",
|
||||
"google": "GOOGLE_API_KEY",
|
||||
"ollama": "(local service)",
|
||||
"grok_xai": "XAI_API_KEY",
|
||||
"groq": "GROQ_API_KEY",
|
||||
"cohere": "COHERE_API_KEY",
|
||||
}
|
||||
for provider, env_var in providers.items():
|
||||
status = "✅" if os.environ.get(env_var.replace("(local service)", "")) else "❌"
|
||||
if env_var == "(local service)":
|
||||
status = "🔧"
|
||||
typer.echo(f" {provider:12} {env_var:25} {status}")
|
||||
typer.echo("")
|
||||
|
||||
typer.echo("🌐 Environment Variables:")
|
||||
env_vars = {
|
||||
"PRAISONAI_TEST_TIER": os.environ.get("PRAISONAI_TEST_TIER", "main"),
|
||||
"PRAISONAI_ALLOW_NETWORK": os.environ.get("PRAISONAI_ALLOW_NETWORK", "0"),
|
||||
"PRAISONAI_LIVE_TESTS": os.environ.get("PRAISONAI_LIVE_TESTS", "0"),
|
||||
"PRAISONAI_TEST_PROVIDERS": os.environ.get("PRAISONAI_TEST_PROVIDERS", "openai"),
|
||||
"PRAISONAI_LOCAL_SERVICES": os.environ.get("PRAISONAI_LOCAL_SERVICES", "0"),
|
||||
}
|
||||
for var, value in env_vars.items():
|
||||
typer.echo(f" {var}: {value}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
@@ -57,7 +57,7 @@ class CloudDeployer:
|
||||
file.write("FROM python:3.11-slim\n")
|
||||
file.write("WORKDIR /app\n")
|
||||
file.write("COPY . .\n")
|
||||
file.write("RUN pip install flask praisonai==3.5.2 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==3.5.4 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -6,22 +6,43 @@ CREWAI_AVAILABLE = importlib.util.find_spec("crewai") is not None
|
||||
AUTOGEN_AVAILABLE = importlib.util.find_spec("autogen") is not None
|
||||
PRAISONAI_TOOLS_AVAILABLE = False
|
||||
|
||||
# Guard against recursive imports
|
||||
_importing_autogen_tools = False
|
||||
_autogen_tools_module = None
|
||||
|
||||
# Lazy import helper - only imports when actually needed
|
||||
def _get_autogen_tools():
|
||||
"""Lazy import autogen_tools only when needed."""
|
||||
global PRAISONAI_TOOLS_AVAILABLE
|
||||
global PRAISONAI_TOOLS_AVAILABLE, _importing_autogen_tools, _autogen_tools_module
|
||||
|
||||
# Return cached module if already imported
|
||||
if _autogen_tools_module is not None:
|
||||
return _autogen_tools_module
|
||||
|
||||
# Prevent recursive import
|
||||
if _importing_autogen_tools:
|
||||
return None
|
||||
|
||||
if CREWAI_AVAILABLE or AUTOGEN_AVAILABLE:
|
||||
try:
|
||||
_importing_autogen_tools = True
|
||||
from . import autogen_tools
|
||||
_autogen_tools_module = autogen_tools
|
||||
PRAISONAI_TOOLS_AVAILABLE = True
|
||||
return autogen_tools
|
||||
except ImportError:
|
||||
pass
|
||||
finally:
|
||||
_importing_autogen_tools = False
|
||||
return None
|
||||
|
||||
# For backward compatibility, provide __getattr__ for lazy access
|
||||
def __getattr__(name):
|
||||
"""Lazy load autogen_tools exports on demand."""
|
||||
# Avoid recursion during import
|
||||
if _importing_autogen_tools:
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
tools_module = _get_autogen_tools()
|
||||
if tools_module and hasattr(tools_module, name):
|
||||
return getattr(tools_module, name)
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.5.2"
|
||||
__version__ = "3.5.4"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=0.10.1",
|
||||
"praisonaiagents>=0.10.3",
|
||||
"python-dotenv>=0.19.0",
|
||||
"instructor>=1.3.3",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
@@ -5,10 +5,34 @@ testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
addopts = -v --tb=short
|
||||
addopts = -v --tb=short --timeout=60
|
||||
timeout_method = thread
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning
|
||||
ignore::PendingDeprecationWarning
|
||||
|
||||
markers =
|
||||
slow: marks tests as slow (deselect with '-m "not slow"')
|
||||
integration: marks tests as integration tests
|
||||
unit: marks tests as unit tests
|
||||
asyncio: marks tests as async tests
|
||||
real: marks tests that make real API calls and incur costs
|
||||
# Test type markers (mutually exclusive)
|
||||
unit: Pure unit tests - no network, no side effects
|
||||
integration: Local integration tests - may use filesystem/db
|
||||
e2e: End-to-end tests - external calls allowed if gated
|
||||
|
||||
# Provider markers (additive - auto-applied by conftest)
|
||||
provider_openai: Requires OpenAI API key (OPENAI_API_KEY)
|
||||
provider_anthropic: Requires Anthropic API key (ANTHROPIC_API_KEY)
|
||||
provider_google: Requires Google/Gemini API key (GOOGLE_API_KEY)
|
||||
provider_ollama: Requires Ollama running locally
|
||||
provider_grok_xai: Requires xAI/Grok API key (XAI_API_KEY)
|
||||
provider_groq: Requires Groq API key (GROQ_API_KEY)
|
||||
provider_cohere: Requires Cohere API key (COHERE_API_KEY)
|
||||
|
||||
# Infrastructure markers
|
||||
network: Makes outbound network calls
|
||||
local_service: Requires Docker or local service running
|
||||
slow: Takes more than 5 seconds consistently
|
||||
flaky: Known intermittent failures - retries allowed
|
||||
allow_sleep: Opt out of fast_sleep fixture
|
||||
|
||||
# Legacy markers (kept for backward compatibility)
|
||||
asyncio: Async test support
|
||||
real: Alias for network + provider tests (legacy)
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"generated_at": "2026-01-09T06:18:00Z",
|
||||
"baseline_metrics": {
|
||||
"praisonai_wrapper": {
|
||||
"test_files": 182,
|
||||
"tests_collected": 2896,
|
||||
"collection_errors": 3,
|
||||
"unit_tests_runtime_baseline": "2m0s"
|
||||
},
|
||||
"praisonai_agents": {
|
||||
"test_files": 149,
|
||||
"tests_collected": 2453,
|
||||
"collection_errors": 9
|
||||
}
|
||||
},
|
||||
"after_optimization": {
|
||||
"praisonai_wrapper": {
|
||||
"tests_collected": 2896,
|
||||
"unit_tests_runtime_with_parallel": "1m30s",
|
||||
"smoke_tier_target": "<=2min",
|
||||
"main_tier_target": "<=5min"
|
||||
}
|
||||
},
|
||||
"provider_references": {
|
||||
"praisonai": {
|
||||
"openai": 34,
|
||||
"anthropic": 10,
|
||||
"ollama": 6,
|
||||
"google_gemini": 16,
|
||||
"grok_xai": 1,
|
||||
"groq": 1,
|
||||
"cohere": 3
|
||||
}
|
||||
},
|
||||
"runtime_patterns": {
|
||||
"praisonai": {
|
||||
"files_with_sleep": 14,
|
||||
"files_with_subprocess": 21,
|
||||
"files_with_http_clients": 6
|
||||
}
|
||||
},
|
||||
"sleep_offenders": [
|
||||
"tests/integration/acp/test_acp_stdio.py",
|
||||
"tests/unit/test_daemon_manager.py",
|
||||
"tests/unit/test_profiler.py",
|
||||
"tests/unit/mcp_server/test_auth.py",
|
||||
"tests/unit/acp/test_session.py",
|
||||
"tests/unit/test_profiler_advanced.py",
|
||||
"tests/unit/doctor/test_engine.py",
|
||||
"tests/unit/cli/test_message_queue.py",
|
||||
"tests/unit/scheduler/test_agent_scheduler.py"
|
||||
],
|
||||
"target_tiers": {
|
||||
"smoke": {
|
||||
"target_runtime": "<=2min",
|
||||
"scope": "unit tests, no network, no slow"
|
||||
},
|
||||
"main": {
|
||||
"target_runtime": "<=5min",
|
||||
"scope": "unit + integration, OpenAI gated only"
|
||||
},
|
||||
"extended": {
|
||||
"target_runtime": "<=15min",
|
||||
"scope": "all providers, gated by keys"
|
||||
},
|
||||
"nightly": {
|
||||
"target_runtime": "<=30min",
|
||||
"scope": "full matrix + stress tests"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"decision": "SKIP",
|
||||
"rationale": "Record/replay (VCR.py or similar) adds complexity without sufficient benefit for this codebase",
|
||||
"reasons": [
|
||||
"OpenAI responses are non-deterministic, making cassette matching unreliable",
|
||||
"API response formats change frequently, requiring cassette maintenance",
|
||||
"Most provider tests are already gated and skipped by default",
|
||||
"The fast_sleep fixture and network guard provide sufficient speedup",
|
||||
"Risk of committing secrets in cassettes outweighs benefits"
|
||||
],
|
||||
"alternatives_implemented": [
|
||||
"Network guard blocks outbound calls in unit tests",
|
||||
"Provider tests are gated by environment variables",
|
||||
"fast_sleep fixture accelerates sleep-heavy tests",
|
||||
"Smoke/main tiers exclude network tests by default"
|
||||
],
|
||||
"future_consideration": "If OpenAI live tests become a bottleneck (>5 min), revisit with redacted cassettes"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# PraisonAI Test Plugins
|
||||
# This package contains pytest plugins for test gating and optimization
|
||||
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Network Guard Plugin for PraisonAI Tests
|
||||
|
||||
This module provides a deterministic network blocker that prevents
|
||||
outbound network calls in tests that are not explicitly marked as
|
||||
network-enabled.
|
||||
|
||||
The guard blocks socket connections unless:
|
||||
1. The test is marked with @pytest.mark.network or @pytest.mark.provider_*
|
||||
2. PRAISONAI_ALLOW_NETWORK=1 or PRAISONAI_LIVE_TESTS=1 is set
|
||||
3. The connection is to localhost (127.0.0.1 or ::1)
|
||||
"""
|
||||
|
||||
import os
|
||||
import socket
|
||||
from functools import wraps
|
||||
from typing import Set
|
||||
import pytest
|
||||
|
||||
# Original socket methods (saved before patching)
|
||||
_original_socket_connect = None
|
||||
_original_socket_connect_ex = None
|
||||
|
||||
# Set of test items that are allowed network access
|
||||
_network_allowed_items: Set[str] = set()
|
||||
|
||||
|
||||
class NetworkBlockedError(Exception):
|
||||
"""Raised when a test attempts network access without permission."""
|
||||
pass
|
||||
|
||||
|
||||
def _is_localhost(address):
|
||||
"""Check if an address is localhost."""
|
||||
if isinstance(address, tuple) and len(address) >= 1:
|
||||
host = address[0]
|
||||
return host in ('127.0.0.1', '::1', 'localhost', '0.0.0.0')
|
||||
return False
|
||||
|
||||
|
||||
def _create_blocking_connect(original_connect):
|
||||
"""Create a blocking connect wrapper."""
|
||||
@wraps(original_connect)
|
||||
def blocking_connect(self, address):
|
||||
# Allow localhost connections
|
||||
if _is_localhost(address):
|
||||
return original_connect(self, address)
|
||||
|
||||
# Check if we're in a test context that allows network
|
||||
# This is a simplified check - the main gating happens in conftest
|
||||
if os.environ.get('PRAISONAI_ALLOW_NETWORK') == '1':
|
||||
return original_connect(self, address)
|
||||
if os.environ.get('PRAISONAI_LIVE_TESTS') == '1':
|
||||
return original_connect(self, address)
|
||||
|
||||
# Block the connection
|
||||
raise NetworkBlockedError(
|
||||
f"Network access blocked: attempted connection to {address}. "
|
||||
"Set PRAISONAI_ALLOW_NETWORK=1 or mark test with @pytest.mark.network"
|
||||
)
|
||||
return blocking_connect
|
||||
|
||||
|
||||
def _create_blocking_connect_ex(original_connect_ex):
|
||||
"""Create a blocking connect_ex wrapper."""
|
||||
@wraps(original_connect_ex)
|
||||
def blocking_connect_ex(self, address):
|
||||
# Allow localhost connections
|
||||
if _is_localhost(address):
|
||||
return original_connect_ex(self, address)
|
||||
|
||||
# Check if we're in a test context that allows network
|
||||
if os.environ.get('PRAISONAI_ALLOW_NETWORK') == '1':
|
||||
return original_connect_ex(self, address)
|
||||
if os.environ.get('PRAISONAI_LIVE_TESTS') == '1':
|
||||
return original_connect_ex(self, address)
|
||||
|
||||
# Return connection refused error code instead of raising
|
||||
return 111 # ECONNREFUSED
|
||||
return blocking_connect_ex
|
||||
|
||||
|
||||
def install_network_guard():
|
||||
"""Install the network guard by patching socket methods."""
|
||||
global _original_socket_connect, _original_socket_connect_ex
|
||||
|
||||
if _original_socket_connect is None:
|
||||
_original_socket_connect = socket.socket.connect
|
||||
socket.socket.connect = _create_blocking_connect(_original_socket_connect)
|
||||
|
||||
if _original_socket_connect_ex is None:
|
||||
_original_socket_connect_ex = socket.socket.connect_ex
|
||||
socket.socket.connect_ex = _create_blocking_connect_ex(_original_socket_connect_ex)
|
||||
|
||||
|
||||
def uninstall_network_guard():
|
||||
"""Uninstall the network guard by restoring original socket methods."""
|
||||
global _original_socket_connect, _original_socket_connect_ex
|
||||
|
||||
if _original_socket_connect is not None:
|
||||
socket.socket.connect = _original_socket_connect
|
||||
_original_socket_connect = None
|
||||
|
||||
if _original_socket_connect_ex is not None:
|
||||
socket.socket.connect_ex = _original_socket_connect_ex
|
||||
_original_socket_connect_ex = None
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def network_guard_fixture(request):
|
||||
"""
|
||||
Fixture that manages network guard state per test.
|
||||
|
||||
Network is blocked by default unless:
|
||||
- Test is marked with network, real, or provider_* markers
|
||||
- PRAISONAI_ALLOW_NETWORK=1 or PRAISONAI_LIVE_TESTS=1
|
||||
"""
|
||||
# Check if network should be allowed for this test
|
||||
allow_network = False
|
||||
|
||||
# Check environment variables
|
||||
if os.environ.get('PRAISONAI_ALLOW_NETWORK') == '1':
|
||||
allow_network = True
|
||||
if os.environ.get('PRAISONAI_LIVE_TESTS') == '1':
|
||||
allow_network = True
|
||||
|
||||
# Check markers
|
||||
if hasattr(request, 'node') and hasattr(request.node, 'iter_markers'):
|
||||
for marker in request.node.iter_markers():
|
||||
if marker.name in ('network', 'real', 'e2e') or marker.name.startswith('provider_'):
|
||||
allow_network = True
|
||||
break
|
||||
|
||||
# Check path
|
||||
if hasattr(request, 'fspath') and request.fspath:
|
||||
test_path = str(request.fspath)
|
||||
if any(p in test_path for p in ['/integration/', '/e2e/', '/live/']):
|
||||
allow_network = True
|
||||
|
||||
if allow_network:
|
||||
# Temporarily uninstall guard for this test
|
||||
uninstall_network_guard()
|
||||
yield
|
||||
# Re-install after test
|
||||
install_network_guard()
|
||||
else:
|
||||
# Keep guard installed
|
||||
yield
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Install network guard at session start."""
|
||||
# Only install if not explicitly allowing network
|
||||
if os.environ.get('PRAISONAI_ALLOW_NETWORK') != '1' and os.environ.get('PRAISONAI_LIVE_TESTS') != '1':
|
||||
install_network_guard()
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
"""Uninstall network guard at session end."""
|
||||
uninstall_network_guard()
|
||||
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
PraisonAI Test Gating Plugin
|
||||
|
||||
This module provides automatic marker assignment and skip/gating enforcement
|
||||
for the PraisonAI test suite. It ensures tests are properly classified and
|
||||
gated based on provider requirements and network access.
|
||||
|
||||
Environment Variables:
|
||||
- PRAISONAI_TEST_TIER: smoke|main|extended|nightly (default: main)
|
||||
- PRAISONAI_ALLOW_NETWORK: 0|1 (default: 0)
|
||||
- PRAISONAI_LIVE_TESTS: 0|1 (default: 0)
|
||||
- PRAISONAI_TEST_PROVIDERS: comma-separated list or 'all' (default: openai)
|
||||
- PRAISONAI_LOCAL_SERVICES: 0|1 (default: 0)
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
from pathlib import Path
|
||||
from typing import Set, Dict, Optional
|
||||
import pytest
|
||||
|
||||
# Provider detection patterns (case-insensitive)
|
||||
PROVIDER_PATTERNS: Dict[str, re.Pattern] = {
|
||||
'provider_openai': re.compile(r'\b(openai|gpt-[34]|gpt4|chatgpt)\b', re.IGNORECASE),
|
||||
'provider_anthropic': re.compile(r'\b(anthropic|claude)\b', re.IGNORECASE),
|
||||
'provider_google': re.compile(r'\b(google|gemini|palm|vertex)\b', re.IGNORECASE),
|
||||
'provider_ollama': re.compile(r'\b(ollama)\b', re.IGNORECASE),
|
||||
'provider_grok_xai': re.compile(r'\b(grok|xai|x\.ai)\b', re.IGNORECASE),
|
||||
'provider_groq': re.compile(r'\b(groq)\b', re.IGNORECASE),
|
||||
'provider_cohere': re.compile(r'\b(cohere)\b', re.IGNORECASE),
|
||||
}
|
||||
|
||||
# Provider to environment variable mapping
|
||||
PROVIDER_ENV_KEYS: Dict[str, str] = {
|
||||
'provider_openai': 'OPENAI_API_KEY',
|
||||
'provider_anthropic': 'ANTHROPIC_API_KEY',
|
||||
'provider_google': 'GOOGLE_API_KEY',
|
||||
'provider_ollama': None, # Requires service check
|
||||
'provider_grok_xai': 'XAI_API_KEY',
|
||||
'provider_groq': 'GROQ_API_KEY',
|
||||
'provider_cohere': 'COHERE_API_KEY',
|
||||
}
|
||||
|
||||
# Cache for file content scans (avoid re-reading files)
|
||||
_file_content_cache: Dict[str, str] = {}
|
||||
|
||||
|
||||
def _get_test_tier() -> str:
|
||||
"""Get the current test tier from environment."""
|
||||
return os.environ.get('PRAISONAI_TEST_TIER', 'main').lower()
|
||||
|
||||
|
||||
def _is_network_allowed() -> bool:
|
||||
"""Check if network access is allowed."""
|
||||
return (
|
||||
os.environ.get('PRAISONAI_ALLOW_NETWORK', '0') == '1' or
|
||||
os.environ.get('PRAISONAI_LIVE_TESTS', '0') == '1'
|
||||
)
|
||||
|
||||
|
||||
def _get_allowed_providers() -> Set[str]:
|
||||
"""Get the set of allowed providers."""
|
||||
providers_str = os.environ.get('PRAISONAI_TEST_PROVIDERS', 'openai')
|
||||
if providers_str.lower() == 'all':
|
||||
return set(PROVIDER_ENV_KEYS.keys())
|
||||
return {f'provider_{p.strip().lower()}' for p in providers_str.split(',')}
|
||||
|
||||
|
||||
def _is_local_services_allowed() -> bool:
|
||||
"""Check if local services (Docker, etc.) are allowed."""
|
||||
return os.environ.get('PRAISONAI_LOCAL_SERVICES', '0') == '1'
|
||||
|
||||
|
||||
def _check_ollama_available() -> bool:
|
||||
"""Check if Ollama is running locally."""
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(0.5)
|
||||
result = sock.connect_ex(('127.0.0.1', 11434))
|
||||
sock.close()
|
||||
return result == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _check_provider_available(provider_marker: str) -> tuple[bool, str]:
|
||||
"""
|
||||
Check if a provider is available.
|
||||
Returns (is_available, reason_if_not).
|
||||
"""
|
||||
env_key = PROVIDER_ENV_KEYS.get(provider_marker)
|
||||
|
||||
if provider_marker == 'provider_ollama':
|
||||
if _check_ollama_available():
|
||||
return True, ""
|
||||
return False, "Ollama not running on localhost:11434"
|
||||
|
||||
if env_key:
|
||||
if os.environ.get(env_key):
|
||||
return True, ""
|
||||
return False, f"{env_key} not set"
|
||||
|
||||
return True, "" # Unknown provider, allow by default
|
||||
|
||||
|
||||
def _get_file_content(filepath: Path) -> str:
|
||||
"""Get file content with caching."""
|
||||
filepath_str = str(filepath)
|
||||
if filepath_str not in _file_content_cache:
|
||||
try:
|
||||
_file_content_cache[filepath_str] = filepath.read_text(errors='ignore')
|
||||
except Exception:
|
||||
_file_content_cache[filepath_str] = ""
|
||||
return _file_content_cache[filepath_str]
|
||||
|
||||
|
||||
def _detect_providers_in_file(filepath: Path) -> Set[str]:
|
||||
"""Detect which providers are referenced in a test file."""
|
||||
# Skip detection for plugin test files and meta directories
|
||||
filepath_str = str(filepath)
|
||||
if '_pytest_plugins' in filepath_str or '_meta' in filepath_str:
|
||||
return set()
|
||||
if 'test_test_command' in filepath_str or 'test_gating' in filepath_str:
|
||||
return set()
|
||||
|
||||
content = _get_file_content(filepath)
|
||||
detected = set()
|
||||
for marker, pattern in PROVIDER_PATTERNS.items():
|
||||
if pattern.search(content):
|
||||
detected.add(marker)
|
||||
return detected
|
||||
|
||||
|
||||
def _get_test_type_from_path(nodeid: str) -> Optional[str]:
|
||||
"""Determine test type based on path conventions."""
|
||||
nodeid_lower = nodeid.lower()
|
||||
if '/unit/' in nodeid_lower or '\\unit\\' in nodeid_lower:
|
||||
return 'unit'
|
||||
if '/integration/' in nodeid_lower or '\\integration\\' in nodeid_lower:
|
||||
return 'integration'
|
||||
if '/e2e/' in nodeid_lower or '\\e2e\\' in nodeid_lower:
|
||||
return 'e2e'
|
||||
if '/live/' in nodeid_lower or '\\live\\' in nodeid_lower:
|
||||
return 'e2e'
|
||||
return None
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Register custom markers and initialize plugin state."""
|
||||
# Clear file content cache at start of session
|
||||
_file_content_cache.clear()
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
"""
|
||||
Auto-assign markers and apply skip logic based on gating rules.
|
||||
|
||||
This hook runs after test collection and:
|
||||
1. Adds test type markers (unit/integration/e2e) based on path
|
||||
2. Adds provider markers based on file content analysis
|
||||
3. Adds network marker if any provider marker is present
|
||||
4. Applies skip logic based on environment configuration
|
||||
"""
|
||||
tier = _get_test_tier()
|
||||
network_allowed = _is_network_allowed()
|
||||
allowed_providers = _get_allowed_providers()
|
||||
local_services_allowed = _is_local_services_allowed()
|
||||
|
||||
for item in items:
|
||||
# Get existing markers
|
||||
existing_markers = {m.name for m in item.iter_markers()}
|
||||
|
||||
# 1. Auto-assign test type marker based on path
|
||||
test_type = _get_test_type_from_path(item.nodeid)
|
||||
if test_type and test_type not in existing_markers:
|
||||
item.add_marker(getattr(pytest.mark, test_type))
|
||||
|
||||
# 2. Auto-detect and assign provider markers from file content
|
||||
if item.fspath:
|
||||
filepath = Path(item.fspath)
|
||||
detected_providers = _detect_providers_in_file(filepath)
|
||||
|
||||
# Also check nodeid for provider keywords
|
||||
for marker, pattern in PROVIDER_PATTERNS.items():
|
||||
if pattern.search(item.nodeid):
|
||||
detected_providers.add(marker)
|
||||
|
||||
for provider in detected_providers:
|
||||
if provider not in existing_markers:
|
||||
item.add_marker(getattr(pytest.mark, provider))
|
||||
|
||||
# Refresh existing markers after additions
|
||||
existing_markers = {m.name for m in item.iter_markers()}
|
||||
|
||||
# 3. Add network marker if any provider marker is present
|
||||
provider_markers = {m for m in existing_markers if m.startswith('provider_')}
|
||||
if provider_markers and 'network' not in existing_markers:
|
||||
item.add_marker(pytest.mark.network)
|
||||
|
||||
# Handle 'real' marker as alias for network
|
||||
if 'real' in existing_markers and 'network' not in existing_markers:
|
||||
item.add_marker(pytest.mark.network)
|
||||
|
||||
# Refresh markers again
|
||||
existing_markers = {m.name for m in item.iter_markers()}
|
||||
|
||||
# 4. Apply skip logic based on tier and gating rules
|
||||
|
||||
# Smoke tier: only unit tests, no network, no slow
|
||||
if tier == 'smoke':
|
||||
if 'integration' in existing_markers or 'e2e' in existing_markers:
|
||||
item.add_marker(pytest.mark.skip(
|
||||
reason="Smoke tier: skipping non-unit tests"
|
||||
))
|
||||
continue
|
||||
if 'slow' in existing_markers:
|
||||
item.add_marker(pytest.mark.skip(
|
||||
reason="Smoke tier: skipping slow tests"
|
||||
))
|
||||
continue
|
||||
|
||||
# Skip network tests if network not allowed
|
||||
if 'network' in existing_markers and not network_allowed:
|
||||
item.add_marker(pytest.mark.skip(
|
||||
reason="Network tests disabled. Set PRAISONAI_ALLOW_NETWORK=1 or PRAISONAI_LIVE_TESTS=1"
|
||||
))
|
||||
continue
|
||||
|
||||
# Skip provider tests if provider not in allowed list or key missing
|
||||
for provider in provider_markers:
|
||||
if provider not in allowed_providers:
|
||||
item.add_marker(pytest.mark.skip(
|
||||
reason=f"Provider {provider} not in PRAISONAI_TEST_PROVIDERS"
|
||||
))
|
||||
break
|
||||
|
||||
# Check if provider is actually available
|
||||
if network_allowed:
|
||||
available, reason = _check_provider_available(provider)
|
||||
if not available:
|
||||
item.add_marker(pytest.mark.skip(reason=reason))
|
||||
break
|
||||
|
||||
# Skip local_service tests if not allowed
|
||||
if 'local_service' in existing_markers and not local_services_allowed:
|
||||
item.add_marker(pytest.mark.skip(
|
||||
reason="Local service tests disabled. Set PRAISONAI_LOCAL_SERVICES=1"
|
||||
))
|
||||
continue
|
||||
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus):
|
||||
"""Clean up at end of session."""
|
||||
_file_content_cache.clear()
|
||||
@@ -5,10 +5,13 @@ import asyncio
|
||||
import warnings
|
||||
import gc
|
||||
from unittest.mock import Mock, patch
|
||||
from typing import Dict, Any, List
|
||||
|
||||
# Register pytest-asyncio plugin
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
# Register pytest plugins
|
||||
pytest_plugins = (
|
||||
'pytest_asyncio',
|
||||
'tests._pytest_plugins.test_gating',
|
||||
'tests._pytest_plugins.network_guard',
|
||||
)
|
||||
|
||||
# Suppress aiohttp unclosed session warnings during tests
|
||||
warnings.filterwarnings("ignore", message="Unclosed client session")
|
||||
@@ -114,20 +117,22 @@ def setup_test_environment(request):
|
||||
# Real tests (marked with @pytest.mark.real) should use actual environment variables
|
||||
is_real_test = False
|
||||
|
||||
# Check if this test is marked as a real test or integration test
|
||||
# Check if this test is marked as a real test, network test, or provider test
|
||||
if hasattr(request, 'node') and hasattr(request.node, 'iter_markers'):
|
||||
for marker in request.node.iter_markers():
|
||||
if marker.name in ('real', 'integration'):
|
||||
if marker.name in ('real', 'network', 'e2e') or marker.name.startswith('provider_'):
|
||||
is_real_test = True
|
||||
break
|
||||
|
||||
# Also check if the test file is in the integration directory
|
||||
# Also check if the test file is in the integration/live/e2e directory
|
||||
if hasattr(request, 'fspath') and request.fspath:
|
||||
test_path = str(request.fspath)
|
||||
if '/integration/' in test_path or '\\integration\\' in test_path:
|
||||
is_real_test = True
|
||||
if '/live/' in test_path or '\\live\\' in test_path:
|
||||
is_real_test = True
|
||||
if '/e2e/' in test_path or '\\e2e\\' in test_path:
|
||||
is_real_test = True
|
||||
|
||||
# Store original values to restore later
|
||||
original_values = {}
|
||||
@@ -137,7 +142,10 @@ def setup_test_environment(request):
|
||||
test_keys = {
|
||||
'OPENAI_API_KEY': 'test-key',
|
||||
'ANTHROPIC_API_KEY': 'test-key',
|
||||
'GOOGLE_API_KEY': 'test-key'
|
||||
'GOOGLE_API_KEY': 'test-key',
|
||||
'XAI_API_KEY': 'test-key',
|
||||
'GROQ_API_KEY': 'test-key',
|
||||
'COHERE_API_KEY': 'test-key',
|
||||
}
|
||||
|
||||
for key, value in test_keys.items():
|
||||
@@ -152,4 +160,42 @@ def setup_test_environment(request):
|
||||
if original_value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = original_value
|
||||
os.environ[key] = original_value
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fast_sleep(request, monkeypatch):
|
||||
"""
|
||||
Replace time.sleep and asyncio.sleep with near-instant versions for unit tests.
|
||||
|
||||
This fixture is automatically applied to all tests but can be disabled by:
|
||||
- Marking the test with @pytest.mark.allow_sleep
|
||||
- Marking the test with @pytest.mark.integration, @pytest.mark.e2e, or @pytest.mark.local_service
|
||||
- Marking the test with @pytest.mark.flaky
|
||||
"""
|
||||
# Check if test should use real sleep
|
||||
if hasattr(request, 'node') and hasattr(request.node, 'iter_markers'):
|
||||
for marker in request.node.iter_markers():
|
||||
if marker.name in ('allow_sleep', 'integration', 'e2e', 'local_service', 'flaky'):
|
||||
return # Don't patch sleep for these tests
|
||||
|
||||
# Check path-based exclusions
|
||||
if hasattr(request, 'fspath') and request.fspath:
|
||||
test_path = str(request.fspath)
|
||||
if any(p in test_path for p in ['/integration/', '/e2e/', '/live/']):
|
||||
return # Don't patch sleep for integration/e2e/live tests
|
||||
|
||||
import time
|
||||
import asyncio
|
||||
|
||||
# Patch time.sleep to be near-instant (0.001s max)
|
||||
original_sleep = time.sleep
|
||||
def fast_time_sleep(seconds):
|
||||
original_sleep(min(seconds, 0.001))
|
||||
monkeypatch.setattr(time, 'sleep', fast_time_sleep)
|
||||
|
||||
# Patch asyncio.sleep to be near-instant
|
||||
original_async_sleep = asyncio.sleep
|
||||
async def fast_async_sleep(seconds):
|
||||
await original_async_sleep(min(seconds, 0.001))
|
||||
monkeypatch.setattr(asyncio, 'sleep', fast_async_sleep)
|
||||
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
Unit tests for the praisonai test CLI command.
|
||||
|
||||
Tests the test tier and provider options without actually running pytest.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Import the module under test
|
||||
from praisonai.cli.commands.test import (
|
||||
_get_pytest_args,
|
||||
_set_environment,
|
||||
)
|
||||
|
||||
|
||||
class TestGetPytestArgs:
|
||||
"""Tests for _get_pytest_args function."""
|
||||
|
||||
def test_smoke_tier_args(self):
|
||||
"""Smoke tier should target unit tests only with no slow/network."""
|
||||
args = _get_pytest_args(
|
||||
tier="smoke",
|
||||
provider=None,
|
||||
live=False,
|
||||
parallel=None,
|
||||
verbose=False,
|
||||
coverage=False,
|
||||
)
|
||||
assert "tests/unit/" in args
|
||||
assert "-m" in args
|
||||
assert "not slow and not network" in args
|
||||
assert "--timeout=30" in args
|
||||
|
||||
def test_main_tier_args(self):
|
||||
"""Main tier should exclude non-OpenAI providers."""
|
||||
args = _get_pytest_args(
|
||||
tier="main",
|
||||
provider=None,
|
||||
live=False,
|
||||
parallel=None,
|
||||
verbose=False,
|
||||
coverage=False,
|
||||
)
|
||||
assert "tests/unit/" in args
|
||||
assert "tests/integration/" in args
|
||||
assert "-m" in args
|
||||
# Should exclude non-OpenAI providers - find the marker expression
|
||||
# The -m flag may appear multiple times, find the one with provider exclusions
|
||||
found_exclusion = False
|
||||
for i, arg in enumerate(args):
|
||||
if arg == "-m" and i + 1 < len(args):
|
||||
marker_expr = args[i + 1]
|
||||
if "provider_anthropic" in marker_expr and "provider_google" in marker_expr:
|
||||
found_exclusion = True
|
||||
break
|
||||
assert found_exclusion, f"Expected provider exclusion markers in args: {args}"
|
||||
|
||||
def test_extended_tier_args(self):
|
||||
"""Extended tier should include all tests."""
|
||||
args = _get_pytest_args(
|
||||
tier="extended",
|
||||
provider=None,
|
||||
live=False,
|
||||
parallel=None,
|
||||
verbose=False,
|
||||
coverage=False,
|
||||
)
|
||||
assert "tests/" in args
|
||||
assert "--timeout=120" in args
|
||||
|
||||
def test_provider_filter(self):
|
||||
"""Provider option should add provider marker filter."""
|
||||
args = _get_pytest_args(
|
||||
tier="main",
|
||||
provider="anthropic",
|
||||
live=False,
|
||||
parallel=None,
|
||||
verbose=False,
|
||||
coverage=False,
|
||||
)
|
||||
assert "-m" in args
|
||||
# Find the provider marker
|
||||
found_provider = False
|
||||
for i, arg in enumerate(args):
|
||||
if arg == "-m" and i + 1 < len(args):
|
||||
if "provider_anthropic" in args[i + 1]:
|
||||
found_provider = True
|
||||
assert found_provider
|
||||
|
||||
def test_parallel_auto(self):
|
||||
"""Parallel auto should add -n auto."""
|
||||
args = _get_pytest_args(
|
||||
tier="main",
|
||||
provider=None,
|
||||
live=False,
|
||||
parallel="auto",
|
||||
verbose=False,
|
||||
coverage=False,
|
||||
)
|
||||
assert "-n" in args
|
||||
assert "auto" in args
|
||||
|
||||
def test_parallel_number(self):
|
||||
"""Parallel with number should add -n <number>."""
|
||||
args = _get_pytest_args(
|
||||
tier="main",
|
||||
provider=None,
|
||||
live=False,
|
||||
parallel="4",
|
||||
verbose=False,
|
||||
coverage=False,
|
||||
)
|
||||
assert "-n" in args
|
||||
assert "4" in args
|
||||
|
||||
def test_verbose_flag(self):
|
||||
"""Verbose should add -v flag."""
|
||||
args = _get_pytest_args(
|
||||
tier="main",
|
||||
provider=None,
|
||||
live=False,
|
||||
parallel=None,
|
||||
verbose=True,
|
||||
coverage=False,
|
||||
)
|
||||
assert "-v" in args
|
||||
assert "-q" not in args
|
||||
|
||||
def test_quiet_by_default(self):
|
||||
"""Non-verbose should add -q flag."""
|
||||
args = _get_pytest_args(
|
||||
tier="main",
|
||||
provider=None,
|
||||
live=False,
|
||||
parallel=None,
|
||||
verbose=False,
|
||||
coverage=False,
|
||||
)
|
||||
assert "-q" in args
|
||||
assert "-v" not in args
|
||||
|
||||
def test_coverage_flag(self):
|
||||
"""Coverage should add coverage args."""
|
||||
args = _get_pytest_args(
|
||||
tier="main",
|
||||
provider=None,
|
||||
live=False,
|
||||
parallel=None,
|
||||
verbose=False,
|
||||
coverage=True,
|
||||
)
|
||||
assert "--cov=praisonai" in args
|
||||
assert "--cov-report=term-missing" in args
|
||||
assert "--cov-report=xml" in args
|
||||
|
||||
def test_always_ignores_fixtures(self):
|
||||
"""All tiers should ignore fixtures directory."""
|
||||
for tier in ["smoke", "main", "extended", "nightly"]:
|
||||
args = _get_pytest_args(
|
||||
tier=tier,
|
||||
provider=None,
|
||||
live=False,
|
||||
parallel=None,
|
||||
verbose=False,
|
||||
coverage=False,
|
||||
)
|
||||
assert "--ignore=tests/fixtures" in args
|
||||
|
||||
|
||||
class TestSetEnvironment:
|
||||
"""Tests for _set_environment function."""
|
||||
|
||||
def test_sets_tier(self):
|
||||
"""Should set PRAISONAI_TEST_TIER."""
|
||||
_set_environment("smoke", None, False)
|
||||
assert os.environ.get("PRAISONAI_TEST_TIER") == "smoke"
|
||||
|
||||
def test_live_enables_network(self):
|
||||
"""Live mode should enable network and live tests."""
|
||||
_set_environment("main", None, True)
|
||||
assert os.environ.get("PRAISONAI_ALLOW_NETWORK") == "1"
|
||||
assert os.environ.get("PRAISONAI_LIVE_TESTS") == "1"
|
||||
|
||||
def test_no_live_disables_network(self):
|
||||
"""Non-live mode should disable network."""
|
||||
_set_environment("main", None, False)
|
||||
assert os.environ.get("PRAISONAI_ALLOW_NETWORK") == "0"
|
||||
assert os.environ.get("PRAISONAI_LIVE_TESTS") == "0"
|
||||
|
||||
def test_sets_provider(self):
|
||||
"""Should set PRAISONAI_TEST_PROVIDERS when provider specified."""
|
||||
_set_environment("main", "anthropic", False)
|
||||
assert os.environ.get("PRAISONAI_TEST_PROVIDERS") == "anthropic"
|
||||
|
||||
|
||||
class TestTestGatingPlugin:
|
||||
"""Tests for the test gating plugin functionality."""
|
||||
|
||||
def test_provider_patterns_detect_openai(self):
|
||||
"""Provider patterns should detect OpenAI references."""
|
||||
from tests._pytest_plugins.test_gating import PROVIDER_PATTERNS
|
||||
|
||||
pattern = PROVIDER_PATTERNS['provider_openai']
|
||||
assert pattern.search("from openai import OpenAI")
|
||||
assert pattern.search("gpt-4 model")
|
||||
assert pattern.search("ChatGPT response")
|
||||
|
||||
def test_provider_patterns_detect_anthropic(self):
|
||||
"""Provider patterns should detect Anthropic references."""
|
||||
from tests._pytest_plugins.test_gating import PROVIDER_PATTERNS
|
||||
|
||||
pattern = PROVIDER_PATTERNS['provider_anthropic']
|
||||
assert pattern.search("from anthropic import Client")
|
||||
assert pattern.search("claude-3 model")
|
||||
|
||||
def test_provider_patterns_detect_ollama(self):
|
||||
"""Provider patterns should detect Ollama references."""
|
||||
from tests._pytest_plugins.test_gating import PROVIDER_PATTERNS
|
||||
|
||||
pattern = PROVIDER_PATTERNS['provider_ollama']
|
||||
assert pattern.search("ollama run llama")
|
||||
assert pattern.search("Ollama client")
|
||||
|
||||
def test_get_test_type_from_path(self):
|
||||
"""Should detect test type from path."""
|
||||
from tests._pytest_plugins.test_gating import _get_test_type_from_path
|
||||
|
||||
assert _get_test_type_from_path("tests/unit/test_foo.py") == "unit"
|
||||
assert _get_test_type_from_path("tests/integration/test_bar.py") == "integration"
|
||||
assert _get_test_type_from_path("tests/e2e/test_baz.py") == "e2e"
|
||||
assert _get_test_type_from_path("tests/live/test_qux.py") == "e2e"
|
||||
assert _get_test_type_from_path("tests/test_random.py") is None
|
||||
|
||||
|
||||
class TestNetworkGuard:
|
||||
"""Tests for the network guard plugin."""
|
||||
|
||||
def test_localhost_allowed(self):
|
||||
"""Localhost connections should always be allowed."""
|
||||
from tests._pytest_plugins.network_guard import _is_localhost
|
||||
|
||||
assert _is_localhost(("127.0.0.1", 8080))
|
||||
assert _is_localhost(("localhost", 80))
|
||||
assert _is_localhost(("::1", 443))
|
||||
assert not _is_localhost(("example.com", 80))
|
||||
assert not _is_localhost(("8.8.8.8", 53))
|
||||
Generated
+4
-4
@@ -4276,7 +4276,7 @@ requires-dist = [
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.1.0" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.1.0" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.1.0" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.10.1" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.10.3" },
|
||||
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
|
||||
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
|
||||
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
|
||||
@@ -4324,7 +4324,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.10.1"
|
||||
version = "0.10.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -4333,9 +4333,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/20/d7c02b4dc2da4c4427046b60286858288c44c5cae25ad08c3fc0727f8ebb/praisonaiagents-0.10.1.tar.gz", hash = "sha256:6d21ce2cbf5f0a4f56798f731a79d703536e1dc8706f1b00915d772e4d4084ea", size = 945938, upload-time = "2026-01-08T21:08:39.706Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/ce/9f55784b2daa903e8c354ebaa0fc6176452b07b095c81ed8c8d082ebbc96/praisonaiagents-0.10.3.tar.gz", hash = "sha256:b25be65dd1723d7fb55ac5a9a24752bceaa0b509605e03865edf15b1c569cb89", size = 945997, upload-time = "2026-01-09T06:51:33.641Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/d6/b381217a998847756ad9b423fc0cb0c2134e09971abfd333698abf809fae/praisonaiagents-0.10.1-py3-none-any.whl", hash = "sha256:6202ed7a3a46d299719a8b0470b48fcbac9f0989ac9fc19406065749ca5f6527", size = 923192, upload-time = "2026-01-08T21:08:37.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/b0/a9dd443fed5a0849bd2fb04a083bc21f04b3d9720da404d183d266a238e5/praisonaiagents-0.10.3-py3-none-any.whl", hash = "sha256:fcdbb6cf938524fe6f8338be200ce83bacb542ea9d0c242d2b8b12aaf3b7b432", size = 923193, upload-time = "2026-01-09T06:51:31.279Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user