Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe87b9514e | ||
|
|
fec0b5fa32 | ||
|
|
c8a3881dda | ||
|
|
bfe3d94bad | ||
|
|
7ae6c6d19c | ||
|
|
cc962c96d4 | ||
|
|
d5a55aaf4b | ||
|
|
a02a7209a9 | ||
|
|
0e55b36056 | ||
|
|
d89e467257 | ||
|
|
1e2a5ef12d | ||
|
|
de069c2cbf | ||
|
|
ff599795f8 | ||
|
|
9164555a7a | ||
|
|
b26c44ad90 | ||
|
|
4ee7d298c8 | ||
|
|
1ae86076a9 | ||
|
|
7def9a68fa | ||
|
|
99cdf47b09 | ||
|
|
7bb1ca97f5 | ||
|
|
9ba435f147 | ||
|
|
8582562e45 | ||
|
|
76c1248e38 | ||
|
|
4ca5867075 | ||
|
|
ce7d9815aa | ||
|
|
6a531ac1da | ||
|
|
cc6116b888 | ||
|
|
d933eebeaf | ||
|
|
2c683af1cd | ||
|
|
a1b6989840 | ||
|
|
98a878673a | ||
|
|
cec46053ab | ||
|
|
ad7445c528 | ||
|
|
3e2909b926 | ||
|
|
ad32470de4 | ||
|
|
5dd9bdb96f | ||
|
|
29b0a6f02e | ||
|
|
7a9788c311 | ||
|
|
055d9f5d88 | ||
|
|
b27ae3a79a | ||
|
|
d0a4796742 | ||
|
|
a3ce0f69e3 | ||
|
|
19929dd9de | ||
|
|
36c2e76ca4 | ||
|
|
a0e4a765ff | ||
|
|
18a3d26620 | ||
|
|
cbec55cf44 | ||
|
|
f96eebb5e8 | ||
|
|
6fa8eebab1 | ||
|
|
9702a2a99c | ||
|
|
969c21f617 | ||
|
|
8b5b67852a | ||
|
|
019ecea1a4 | ||
|
|
fab53bf095 | ||
|
|
d6bcd522ed | ||
|
|
8769a3bdb8 | ||
|
|
bfda2ff1db | ||
|
|
0a7d4187ad | ||
|
|
b0222c4fae | ||
|
|
1c901be114 | ||
|
|
534594103d | ||
|
|
17d7ddd0f7 | ||
|
|
c546d2c2c9 | ||
|
|
13267e5325 | ||
|
|
f188ae1ff9 | ||
|
|
17ca3f8f48 | ||
|
|
ed76058630 | ||
|
|
9b50b4069f | ||
|
|
9ad41a8ec5 | ||
|
|
3f77e93840 | ||
|
|
06eb6b197c | ||
|
|
d476379f45 | ||
|
|
d1034adc19 | ||
|
|
ee8d0cf4c3 | ||
|
|
acad479b19 | ||
|
|
1eb22d74e9 | ||
|
|
381c7448ea | ||
|
|
13c984d1aa | ||
|
|
b04513493e | ||
|
|
3a209d7c36 | ||
|
|
eb15d66e96 | ||
|
|
54ddbaf298 | ||
|
|
18b7dc1687 | ||
|
|
10869d7570 | ||
|
|
c796f75b41 | ||
|
|
79cda2604b | ||
|
|
7659748515 | ||
|
|
aa092c2c3f | ||
|
|
dc69cbc121 | ||
|
|
60121929d0 | ||
|
|
6646dd1a4c | ||
|
|
ba3a3c3ee0 | ||
|
|
b965bb1011 | ||
|
|
4d13ab4af7 | ||
|
|
84c4d221a2 | ||
|
|
64ae40e369 | ||
|
|
0f5849b734 | ||
|
|
3a2a2f8592 | ||
|
|
3193d19a5c | ||
|
|
3468f7743f | ||
|
|
a5dc5fae0e | ||
|
|
ffd78f0eb6 | ||
|
|
727967f459 | ||
|
|
9f45a221e6 | ||
|
|
3924bcea86 | ||
|
|
8dc3136af8 | ||
|
|
0e80a58ccd | ||
|
|
751f9b6f77 | ||
|
|
cec725f7ee | ||
|
|
4804a4d7ee | ||
|
|
8fa99cea83 | ||
|
|
08c40f17d8 | ||
|
|
6120623e21 | ||
|
|
9b365e6594 | ||
|
|
804cacbda4 | ||
|
|
4fe4c27516 | ||
|
|
1af11908de | ||
|
|
c634969263 | ||
|
|
80b10518dc | ||
|
|
259705e230 | ||
|
|
dd41c1ba01 | ||
|
|
e32cccf62a | ||
|
|
de668f1912 | ||
|
|
cfe8277638 | ||
|
|
a8a47c7b7f | ||
|
|
8a803888ea | ||
|
|
e33710e63a | ||
|
|
7f2976c1ff | ||
|
|
0c8ff4b1ee | ||
|
|
d143cce2f9 | ||
|
|
1260ede088 | ||
|
|
2a463d6b39 | ||
|
|
10068583bb | ||
|
|
9bb149a244 | ||
|
|
beb3e0a644 | ||
|
|
6a90cc07ee | ||
|
|
ffb9bd157e | ||
|
|
dd3ccb8b5f | ||
|
|
da8786520d | ||
|
|
ed02fc660f | ||
|
|
3c45e6f474 | ||
|
|
9b85a22b4c | ||
|
|
9a79f2a15d | ||
|
|
11e449bf43 | ||
|
|
d9581f39f8 | ||
|
|
88806ce469 | ||
|
|
468dffa99d | ||
|
|
c1947b9e4b | ||
|
|
21a3774b63 | ||
|
|
2c9b4a5c34 | ||
|
|
bc0a2a3765 | ||
|
|
b6c10e4040 | ||
|
|
f5220973f8 | ||
|
|
a924a55245 | ||
|
|
dcd072dc0c | ||
|
|
8173350f79 | ||
|
|
5b9a046d4b | ||
|
|
27aafddaa4 | ||
|
|
4a0b34ea20 | ||
|
|
19f5a16766 | ||
|
|
20cb053181 | ||
|
|
41ce1de079 | ||
|
|
36920ff8fe | ||
|
|
e428393e0a | ||
|
|
95fd857212 | ||
|
|
c260519bf1 | ||
|
|
2921103ddd | ||
|
|
73800f3509 | ||
|
|
7371ed7048 | ||
|
|
129174257f | ||
|
|
efcdc42fbb | ||
|
|
df79c5acb9 | ||
|
|
d9da7cc59c | ||
|
|
11d9f290c7 | ||
|
|
f796d12908 | ||
|
|
9c4adcee89 | ||
|
|
1d0ffed030 | ||
|
|
3be6df4de8 | ||
|
|
b65de3bf06 | ||
|
|
f949814af8 | ||
|
|
7449ccac76 | ||
|
|
ae26edd3f8 | ||
|
|
6349c88ad9 | ||
|
|
1d4721d039 | ||
|
|
90915fe790 | ||
|
|
29facc8f89 | ||
|
|
401521848f | ||
|
|
ab97eae954 | ||
|
|
5dda0643ca | ||
|
|
1b8d7a02d9 | ||
|
|
dbcd36edce | ||
|
|
9ef5391c83 | ||
|
|
7385adbd60 | ||
|
|
78d7883fb7 | ||
|
|
6122531a6a | ||
|
|
d704cf1349 | ||
|
|
8523ef076e | ||
|
|
72889d2f73 | ||
|
|
f17bd7369c | ||
|
|
ed31e9b280 | ||
|
|
69e9e712c7 | ||
|
|
7617b10c70 | ||
|
|
7939a4f12e | ||
|
|
8cbad0c997 | ||
|
|
48ed131577 | ||
|
|
f8c6f32d00 | ||
|
|
69ff8e7f03 | ||
|
|
feaa81b68f | ||
|
|
39406e904f | ||
|
|
2bb37b1076 | ||
|
|
cc36ac1cf8 | ||
|
|
a041a76f0d | ||
|
|
f77cca9931 | ||
|
|
ef176298b9 | ||
|
|
31f5cbb1cf | ||
|
|
85ec6cba90 | ||
|
|
c77259eda7 | ||
|
|
e69e726ca6 | ||
|
|
3130ae4e19 | ||
|
|
c872be20ed |
No files matched your search
@@ -5,6 +5,9 @@ inputs:
|
||||
anthropic_api_key:
|
||||
description: "Anthropic API key"
|
||||
required: false
|
||||
claude_code_oauth_token:
|
||||
description: "Claude Code OAuth Token for Claude Pro"
|
||||
required: false
|
||||
github_token:
|
||||
description: "GitHub token for Claude to operate with"
|
||||
required: false
|
||||
@@ -212,6 +215,13 @@ runs:
|
||||
- Documentation updates
|
||||
- Testing and deployment
|
||||
|
||||
CRITICAL INSTRUCTION: If your task involves modifying codebase files to add a feature or fix a bug:
|
||||
1. ALWAYS create a new git branch appropriately named (e.g. \`feature/issue-<number>\`).
|
||||
2. MANDATORY: Before writing any code, you MUST carefully read "src/praisonai-agents/AGENTS.md" to understand this repository's extremely specific Architecture, Protocol-First design rules, and Agent-Centric Philosophy.
|
||||
3. Make your necessary code changes honoring the rules in AGENTS.md.
|
||||
4. Commit your changes and push the branch to the remote repository.
|
||||
5. Use the GitHub CLI (\`gh pr create --title "..." --body "Fixes #<number>"\`) to open a Pull Request containing your work.
|
||||
|
||||
Respond naturally and helpfully to the user's request using the available tools.
|
||||
EOF
|
||||
|
||||
@@ -253,4 +263,5 @@ runs:
|
||||
echo "✅ Claude Code execution completed"
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
|
||||
GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||
@@ -6,8 +6,8 @@ inputs:
|
||||
description: "Timeout in minutes for execution"
|
||||
required: false
|
||||
default: "5"
|
||||
anthropic_api_key:
|
||||
description: "Anthropic API key"
|
||||
claude_code_oauth_token:
|
||||
description: "Claude Code OAuth token for Pro subscription access"
|
||||
required: true
|
||||
github_token:
|
||||
description: "GitHub token with repo and issues permissions"
|
||||
@@ -21,7 +21,8 @@ runs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create prompt file
|
||||
- name: Create prompt
|
||||
id: generate_prompt
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p /tmp/claude-prompts
|
||||
@@ -75,13 +76,16 @@ runs:
|
||||
- Your ONLY action should be to apply labels using mcp__github__update_issue
|
||||
- It's okay to not add any labels if none are clearly applicable
|
||||
EOF
|
||||
|
||||
echo "PROMPT<<DELIMITER" >> $GITHUB_OUTPUT
|
||||
cat /tmp/claude-prompts/claude-issue-triage-prompt.txt >> $GITHUB_OUTPUT
|
||||
echo "DELIMITER" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run Claude Code
|
||||
uses: ./.github/actions/claude-code-action
|
||||
with:
|
||||
prompt_file: /tmp/claude-prompts/claude-issue-triage-prompt.txt
|
||||
direct_prompt: ${{ steps.generate_prompt.outputs.PROMPT }}
|
||||
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
|
||||
install_github_mcp: "true"
|
||||
timeout_minutes: ${{ inputs.timeout_minutes }}
|
||||
anthropic_api_key: ${{ inputs.anthropic_api_key }}
|
||||
claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }}
|
||||
github_token: ${{ inputs.github_token }}
|
||||
@@ -22,6 +22,12 @@ inputs:
|
||||
To enable observability with Google Cloud, include:
|
||||
{"telemetry": {"enabled": true, "target": "gcp"}, "sandbox": false}
|
||||
required: false
|
||||
model:
|
||||
description: |
|
||||
The Gemini model to use. Options: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite.
|
||||
Use flash-lite for highest free-tier quota (1,000 RPD).
|
||||
required: false
|
||||
default: 'gemini-3.1-flash-lite-preview'
|
||||
version:
|
||||
description: |
|
||||
The version of @google/gemini-cli to execute.
|
||||
@@ -70,10 +76,11 @@ runs:
|
||||
|
||||
- name: Run Gemini CLI
|
||||
run: |
|
||||
gemini --yolo --prompt "$PROMPT"
|
||||
gemini --yolo --model "$GEMINI_MODEL" --prompt "$PROMPT"
|
||||
shell: bash
|
||||
env:
|
||||
GEMINI_API_KEY: ${{ inputs.GEMINI_API_KEY }}
|
||||
GEMINI_MODEL: ${{ inputs.model }}
|
||||
PROMPT: ${{ inputs.prompt }}
|
||||
|
||||
branding:
|
||||
|
||||
|
After Width: | Height: | Size: 6.8 MiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 285 KiB |
|
Before Width: | Height: | Size: 429 B After Width: | Height: | Size: 429 B |
|
Before Width: | Height: | Size: 872 B After Width: | Height: | Size: 872 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB |
@@ -1,26 +1,46 @@
|
||||
name: Auto Issue Comment with PR Creation
|
||||
name: Auto Issue Comment
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
workflow_dispatch:
|
||||
# issues:
|
||||
# types: [opened]
|
||||
|
||||
jobs:
|
||||
add-comment:
|
||||
# Trigger Claude by adding the 'claude' label (only if not already labeled)
|
||||
# Note: @claude in comments only works from human users (not bots)
|
||||
# The claude.yml workflow listens for issues:labeled event with 'claude' label
|
||||
trigger-claude:
|
||||
# Skip if issue was already created with 'claude' label to avoid double-trigger
|
||||
if: |
|
||||
!contains(toJSON(github.event.issue.labels), 'claude')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: read
|
||||
steps:
|
||||
- name: Add automated comment
|
||||
- name: Add claude label to trigger analysis
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
// Check if 'claude' label already exists on this issue
|
||||
const labels = context.payload.issue.labels.map(l => l.name);
|
||||
if (labels.includes('claude')) {
|
||||
console.log('Issue already has claude label, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add label to trigger claude.yml via issues:labeled event
|
||||
await github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude review this issue and do a detailed analysis and fix this if the existing code doesn\'t have the solution implemented. Making sure it has backward compatibility, no existing features removed. After making those changes, again review the applied changes. Use @web to search if you dont know any information or to find the latest documentation or to find the latest version. Run the code if you think you need to run it to test it. Minimal code change to start with if required any changes. MAINLY IT SHOULD NOT IMPACT ON THE CURRENT SPEED OF EXECUTION of existing features, not increasing the overhead. **Please create a PR using gh tool with your changes**.'
|
||||
})
|
||||
labels: ['claude']
|
||||
});
|
||||
|
||||
// Also post instructions as a comment for context
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '🤖 Auto-triaging with Claude. Analyzing issue and implementing a fix...\n\nClaude will:\n1. Classify this issue\n2. Search codebase for root cause\n3. Implement a minimal fix following AGENTS.md\n4. Create a PR with the changes'
|
||||
});
|
||||
@@ -1,23 +1,142 @@
|
||||
name: Auto PR Comment
|
||||
|
||||
# Review chain: CodeRabbit/Qodo/Gemini review first → THEN trigger Copilot
|
||||
# For human PRs: CodeRabbit/Qodo auto-review → this workflow triggers Copilot
|
||||
# For bot PRs: CodeRabbit/Qodo skip bots, so we trigger them explicitly first
|
||||
#
|
||||
# IMPORTANT: @copilot ignores bot comments. All @copilot mentions must use
|
||||
# PAT_TOKEN so the comment posts as MervinPraison (a real user).
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
pull_request:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
add-comment:
|
||||
# ================================================================
|
||||
# HUMAN PRs: Trigger @copilot AFTER CodeRabbit posts its summary
|
||||
# ================================================================
|
||||
copilot-after-coderabbit:
|
||||
if: |
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
github.event.comment.user.login == 'coderabbitai[bot]' &&
|
||||
contains(github.event.comment.body, 'summary by coderabbit')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Add automated comment
|
||||
- name: Post Copilot review request (as user via PAT)
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
// Check for duplicates — posted by MervinPraison (PAT) or github-actions[bot] (legacy)
|
||||
const alreadyPosted = comments.data.some(c =>
|
||||
c.body.includes('@copilot') &&
|
||||
(c.user.login === 'MervinPraison' || c.user.login === 'github-actions[bot]')
|
||||
);
|
||||
if (alreadyPosted) {
|
||||
console.log('Copilot already triggered, skipping');
|
||||
return;
|
||||
}
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude review this pull request and do a detailed analysis and fix this if the existing code doesn\'t have the solution implemented. Making sure it has backward compatibility, no existing features removed. After making those changes, again review the applied changes. Use @web to search if you dont know any information or to find the latest documentation or to find the latest version. Run the code if you think you need to run it to test it. Minimal code change to start with if required any changes. MAINLY IT SHOULD NOT IMPACT ON THE CURRENT SPEED OF EXECUTION of existing features, not increasing the overhead.'
|
||||
})
|
||||
body: '@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, Coderabbit, and Gemini first — incorporate their findings.\n\nReview areas:\n1. **Bloat check**: Are changes minimal and focused? Any unnecessary code or scope creep?\n2. **Security**: Any hardcoded secrets, unsafe eval/exec, missing input validation?\n3. **Performance**: Any module-level heavy imports? Hot-path regressions?\n4. **Tests**: Are tests included? Do they cover the changes adequately?\n5. **Backward compat**: Any public API changes without deprecation?\n6. **Code quality**: DRY violations, naming conventions, error handling?\n7. **Address reviewer feedback**: If Qodo, Coderabbit, or Gemini flagged valid issues, include them in your review\n8. Suggest specific improvements with code examples where possible'
|
||||
})
|
||||
|
||||
# Fallback: Trigger @copilot after Qodo review (if Coderabbit doesn't fire)
|
||||
copilot-after-qodo:
|
||||
if: |
|
||||
github.event_name == 'pull_request_review' &&
|
||||
github.event.review.user.login == 'qodo-code-review[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Post Copilot review request (as user via PAT)
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c =>
|
||||
c.body.includes('@copilot') &&
|
||||
(c.user.login === 'MervinPraison' || c.user.login === 'github-actions[bot]')
|
||||
);
|
||||
if (alreadyPosted) {
|
||||
console.log('Copilot already triggered, skipping');
|
||||
return;
|
||||
}
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above first.\n\nReview areas:\n1. **Bloat check**: Are changes minimal and focused?\n2. **Security**: Any hardcoded secrets, unsafe eval/exec, missing input validation?\n3. **Performance**: Any module-level heavy imports? Hot-path regressions?\n4. **Tests**: Are tests included? Do they cover the changes adequately?\n5. **Backward compat**: Any public API changes without deprecation?\n6. **Code quality**: DRY violations, naming conventions, error handling?\n7. Suggest specific improvements with code examples where possible'
|
||||
})
|
||||
|
||||
# ================================================================
|
||||
# BOT PRs: CodeRabbit/Qodo skip bot-authored PRs by default.
|
||||
# Trigger them explicitly. Do NOT trigger Copilot here — it will
|
||||
# be triggered by copilot-after-coderabbit/qodo when they finish.
|
||||
# ================================================================
|
||||
bot-pr-trigger-reviews:
|
||||
if: |
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.action == 'opened' &&
|
||||
(
|
||||
github.event.pull_request.user.login == 'github-actions[bot]' ||
|
||||
github.event.pull_request.user.login == 'praisonai-triage-agent[bot]' ||
|
||||
github.event.pull_request.user.type == 'Bot'
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Trigger CodeRabbit, Qodo, and Gemini reviews
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = context.payload.pull_request.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
// 1. Trigger CodeRabbit review
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: pr, owner, repo,
|
||||
body: '@coderabbitai review'
|
||||
});
|
||||
console.log(`Triggered CodeRabbit for bot PR #${pr}`);
|
||||
|
||||
// 2. Trigger Qodo review
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: pr, owner, repo,
|
||||
body: '/review'
|
||||
});
|
||||
console.log(`Triggered Qodo for bot PR #${pr}`);
|
||||
|
||||
// 3. Trigger Gemini review
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: pr, owner, repo,
|
||||
body: '@gemini review this PR'
|
||||
});
|
||||
console.log(`Triggered Gemini for bot PR #${pr}`);
|
||||
|
||||
// NOTE: @copilot is NOT triggered here.
|
||||
// It will be triggered by copilot-after-coderabbit or copilot-after-qodo
|
||||
// once those bots finish, ensuring Copilot always reviews LAST.
|
||||
console.log('Copilot will be triggered after CodeRabbit/Qodo complete.');
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Chain Claude After Reviewers
|
||||
|
||||
# Event-driven: triggers when Copilot finishes reviewing a PR
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
# Trigger Claude after Copilot finishes on a PR — Claude is the FINAL reviewer
|
||||
# Claude reads ALL previous reviews (Qodo, Coderabbit, Copilot) and fixes any valid issues
|
||||
claude-final-review:
|
||||
if: |
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.user.login, 'copilot')) ||
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.user.login, 'copilot'))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Post Claude final review + fix request
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude You are the FINAL architecture reviewer. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n\n**Phase 2: FIX Valid Issues**\n4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n5. Push all code fixes directly to THIS branch (do NOT create a new PR)\n6. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n7. If all issues are resolved, approve the PR / close the Issue\n8. If blocking issues remain, request changes / leave clear action items'
|
||||
})
|
||||
|
||||
# Trigger Claude after Gemini finishes on an Issue or PR
|
||||
claude-after-gemini:
|
||||
if: |
|
||||
github.event_name == 'issue_comment' &&
|
||||
(
|
||||
github.event.comment.user.login == 'gemini-code-assist[bot]' ||
|
||||
contains(github.event.comment.body, 'Review completed by Gemini CLI') ||
|
||||
contains(github.event.comment.body, 'The fix is ready for review. Please test')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Post Claude analysis request (final reviewer)
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude You are the Lead Engineer. Read ALL analysis and reviews above carefully (Gemini, CodeRabbit, Qodo, Copilot, etc).\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n\n**Phase 2: FIX Valid Issues**\n4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n5. Push all code fixes directly to THIS branch (do NOT create a new PR)\n6. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n7. If all issues are resolved, approve the PR / close the Issue\n8. If blocking issues remain, request changes / leave clear action items'
|
||||
})
|
||||
@@ -2,8 +2,9 @@ name: Claude Issue Triage
|
||||
description: "Automatically triage GitHub issues using Claude Code"
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
workflow_dispatch:
|
||||
# issues:
|
||||
# types: [opened]
|
||||
|
||||
jobs:
|
||||
triage-issue:
|
||||
@@ -12,12 +13,20 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.CLAUDE_APP_ID }}
|
||||
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Run Claude Issue Triage
|
||||
uses: ./.github/actions/claude-issue-triage-action
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
github_token: ${{ steps.app-token.outputs.token }}
|
||||
@@ -6,35 +6,85 @@ on:
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned, labeled]
|
||||
types: [assigned, labeled, opened]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude-response:
|
||||
# Prevent certain bots from triggering this action (but allow github-actions[bot] for auto-comments)
|
||||
# Allow: human users, github-actions[bot] (auto-comments/labels)
|
||||
# Block: dependabot, cursor, renovate, other bots
|
||||
if: |
|
||||
(github.event.action != 'labeled' || github.event.label.name == 'claude') &&
|
||||
(github.event_name != 'issue_comment' || contains(github.event.comment.body, '@claude')) &&
|
||||
(
|
||||
!contains(github.actor, '[bot]') ||
|
||||
github.actor == 'github-actions[bot]' ||
|
||||
github.actor == 'praisonai-triage-agent[bot]'
|
||||
) &&
|
||||
github.actor != 'dependabot[bot]' &&
|
||||
github.actor != 'cursor[bot]' &&
|
||||
github.actor != 'renovate[bot]' &&
|
||||
!(contains(github.actor, '[bot]') && github.actor != 'github-actions[bot]')
|
||||
github.actor != 'renovate[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
actions: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.CLAUDE_APP_ID }}
|
||||
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_ACCESS_TOKEN }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
github_token: ${{ steps.app-token.outputs.token }}
|
||||
trigger_phrase: "@claude"
|
||||
label_trigger: "claude"
|
||||
allowed_non_write_users: "*"
|
||||
|
||||
direct_prompt: |
|
||||
You are working on the PraisonAI SDK. Follow AGENTS.md strictly.
|
||||
|
||||
STEP 0 — SETUP GIT IDENTITY:
|
||||
git config user.name "MervinPraison"
|
||||
git config user.email "[email protected]"
|
||||
|
||||
STEP 1 — READ GUIDELINES:
|
||||
Read AGENTS.md to understand the architecture rules.
|
||||
|
||||
STEP 2 — ARCHITECTURE VALIDATION (MANDATORY before writing code):
|
||||
Before implementing anything, answer these questions:
|
||||
- Does this add heavy implementations to the core SDK (praisonaiagents)? If YES → move it to the wrapper (praisonai) or make it a plugin.
|
||||
- Does it duplicate existing functionality? Check if Agent already supports this via existing params (reflection, planning, tools, hooks, memory).
|
||||
- Does it inherit from Agent properly? New agent types MUST inherit Agent, not wrap it with composition.
|
||||
- Does it add new dependencies? Only optional deps allowed, must be lazy-imported.
|
||||
- Will agent.py grow larger? If the change adds >50 lines to agent.py, find a way to extract instead.
|
||||
- Is there a name collision with existing exports in __init__.py?
|
||||
If ANY of these checks fail, add a comment to the issue explaining why and close it. Do NOT create a PR.
|
||||
|
||||
STEP 3 — IMPLEMENT:
|
||||
- Create a fix branch and implement a minimal, focused fix
|
||||
- Follow protocol-driven design: protocols in core SDK, heavy implementations in wrapper
|
||||
- Keep changes small and backward-compatible
|
||||
|
||||
STEP 4 — TEST:
|
||||
- Run: cd src/praisonai-agents && PYTHONPATH=. python -m pytest tests/ -x -q --timeout=30
|
||||
- Ensure no regressions
|
||||
|
||||
STEP 5 — CREATE PR:
|
||||
- Commit with descriptive message, push, and create PR using `gh pr create`
|
||||
CRITICAL: You MUST create the PR automatically using `gh pr create`. Do NOT just provide a link.
|
||||
allowed_tools: |
|
||||
Bash(git:*)
|
||||
Bash(python:*)
|
||||
@@ -55,106 +105,3 @@ jobs:
|
||||
mcp__github__get_issue_comments
|
||||
mcp__github__update_issue
|
||||
timeout_minutes: 30
|
||||
|
||||
post-review-comment:
|
||||
needs: claude-response
|
||||
if: success() && needs.claude-response.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
steps:
|
||||
- name: Check for existing review comment
|
||||
id: check-comment
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const reviewText = '@claude review if the changes made in the above branch are as per the requirements and make changes to the above branch if issues still exist.';
|
||||
let comments = [];
|
||||
let issueNumber;
|
||||
|
||||
try {
|
||||
// Determine issue/PR number based on event type
|
||||
if (context.eventName === 'pull_request_review_comment' || context.eventName === 'pull_request_review') {
|
||||
issueNumber = context.payload.pull_request?.number || context.issue.number;
|
||||
} else if (context.eventName === 'issue_comment' && context.payload.issue.pull_request) {
|
||||
// This is a comment on a PR (not an issue)
|
||||
issueNumber = context.payload.issue.number;
|
||||
} else {
|
||||
// This is a regular issue
|
||||
issueNumber = context.issue.number;
|
||||
}
|
||||
|
||||
comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber
|
||||
});
|
||||
|
||||
// Look for Claude's completed review comments
|
||||
const claudeReviewComments = comments.data.filter(comment =>
|
||||
(comment.user.login === 'claude[bot]' || comment.body.includes('✅') || comment.body.includes('## Summary')) &&
|
||||
!comment.body.includes('⏳') &&
|
||||
!comment.body.includes('🔄') &&
|
||||
!comment.body.includes('Loading...')
|
||||
);
|
||||
|
||||
// Check if our review request comment already exists
|
||||
const existingReviewRequest = comments.data.find(comment =>
|
||||
comment.body.includes(reviewText)
|
||||
);
|
||||
|
||||
// Post if Claude has completed a review AND we haven't posted the review request yet
|
||||
const shouldPost = claudeReviewComments.length > 0 && !existingReviewRequest;
|
||||
core.setOutput('should-post', shouldPost);
|
||||
console.log(`Claude review completed: ${claudeReviewComments.length > 0}`);
|
||||
console.log(`Review request exists: ${!!existingReviewRequest}`);
|
||||
console.log(`Should post comment: ${shouldPost}`);
|
||||
|
||||
} catch (error) {
|
||||
console.log('Error checking comments:', error);
|
||||
core.setOutput('should-post', false); // If we can't check, don't post
|
||||
}
|
||||
|
||||
- name: Wait 30 seconds before posting comment
|
||||
if: steps.check-comment.outputs.should-post == 'true'
|
||||
run: sleep 30
|
||||
|
||||
- name: Post review comment
|
||||
if: steps.check-comment.outputs.should-post == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
let issueNumber;
|
||||
let isPR = false;
|
||||
|
||||
// Determine issue/PR number and type based on event type
|
||||
if (context.eventName === 'pull_request_review_comment' || context.eventName === 'pull_request_review') {
|
||||
issueNumber = context.payload.pull_request?.number || context.issue.number;
|
||||
isPR = true;
|
||||
} else if (context.eventName === 'issue_comment' && context.payload.issue.pull_request) {
|
||||
// This is a comment on a PR (not an issue)
|
||||
issueNumber = context.payload.issue.number;
|
||||
isPR = true;
|
||||
} else {
|
||||
// This is a regular issue - don't post review comment
|
||||
issueNumber = context.issue.number;
|
||||
isPR = false;
|
||||
}
|
||||
|
||||
// Only post review comment on PRs, not regular issues
|
||||
if (isPR) {
|
||||
console.log(`Posting review comment on PR #${issueNumber}`);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: issueNumber,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@claude review if the changes made in the above branch are as per the requirements and make changes to the above branch if issues still exist. MAINLY IT SHOULD NOT IMPACT ON THE CURRENT SPEED OF EXECUTION of existing features, not increasing the overhead.'
|
||||
});
|
||||
} else {
|
||||
console.log(`Skipping review comment for regular issue #${issueNumber}`);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
name: Run tests and upload coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests and collect coverage
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
- name: Install UV
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/praisonai
|
||||
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
uv pip install --system duckduckgo_search
|
||||
uv pip install --system pytest pytest-cov pytest-asyncio
|
||||
# Install knowledge dependencies from praisonai-agents
|
||||
uv pip install --system "praisonaiagents[knowledge]"
|
||||
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_MODEL_NAME=${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}" >> $GITHUB_ENV
|
||||
echo "LOGLEVEL=DEBUG" >> $GITHUB_ENV
|
||||
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd src/praisonai
|
||||
# Run tests - allow some failures for now while we fix remaining issues
|
||||
pytest --cov=praisonai --cov-branch --cov-report=xml tests/unit/ --tb=no -q || echo "Some tests failed, but coverage was collected"
|
||||
|
||||
- name: Upload results to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: src/praisonai/coverage.xml
|
||||
flags: unit-tests
|
||||
name: unit-tests-coverage
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
@@ -1,8 +1,10 @@
|
||||
name: Gemini Automated Issue Triage
|
||||
|
||||
# on:
|
||||
# issue_comment:
|
||||
# types: [created]
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
triage-issue:
|
||||
|
||||
@@ -2,8 +2,8 @@ name: Gemini Issue Review
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
# issue_comment:
|
||||
# types: [created]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -20,7 +20,8 @@ jobs:
|
||||
!contains(github.event.comment.body, '@gemini triage') &&
|
||||
(github.event.comment.author_association == 'OWNER' ||
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR'))
|
||||
github.event.comment.author_association == 'COLLABORATOR' ||
|
||||
github.event.comment.author_association == 'CONTRIBUTOR'))
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
@@ -39,8 +40,8 @@ jobs:
|
||||
|
||||
- name: Set up Git
|
||||
run: |
|
||||
git config --local user.email "gemini-bot@github.com"
|
||||
git config --local user.name "Gemini Bot"
|
||||
git config --local user.email "[email protected].github.com"
|
||||
git config --local user.name "MervinPraison"
|
||||
|
||||
- name: Get issue data
|
||||
id: issue_data
|
||||
@@ -75,16 +76,16 @@ jobs:
|
||||
echo "ADDITIONAL_INSTRUCTIONS=$ADDITIONAL_INSTRUCTIONS" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run Gemini CLI
|
||||
uses: ./.github/actions/gemini
|
||||
uses: google-github-actions/run-gemini-cli@v0
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
ISSUE_NUMBER: ${{ steps.issue_data.outputs.ISSUE_NUMBER }}
|
||||
ADDITIONAL_INSTRUCTIONS: ${{ steps.issue_data.outputs.ADDITIONAL_INSTRUCTIONS }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
with:
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
OTLP_GCP_WIF_PROVIDER: ${{ secrets.OTLP_GCP_WIF_PROVIDER }}
|
||||
OTLP_GOOGLE_CLOUD_PROJECT: ${{ secrets.OTLP_GOOGLE_CLOUD_PROJECT }}
|
||||
gemini_api_key: ${{ secrets.GEMINI_API_KEY }}
|
||||
gemini_model: 'gemini-2.5-flash'
|
||||
prompt: |
|
||||
You are a helpful GitHub issue assistant. Your task is to analyze and help with GitHub issues.
|
||||
|
||||
@@ -103,6 +104,8 @@ jobs:
|
||||
- Search codebase for related files using `glob`, `search_file_content`, `list_directory`
|
||||
|
||||
**STEP 2: If Code Changes Are Needed**
|
||||
MANDATORY FIRST STEP: Before writing any code, you MUST use `read_file` to carefully read "src/praisonai-agents/AGENTS.md". You must honor this repository's extremely specific Architecture, Protocol-First design rules, and Agent-Centric Philosophy!
|
||||
|
||||
Create a new branch with 'gemini/' prefix:
|
||||
- Use: `git checkout -b gemini/issue-${ISSUE_NUMBER}-description`
|
||||
- Example: `git checkout -b gemini/issue-123-fix-sequential-tools`
|
||||
@@ -161,7 +164,7 @@ jobs:
|
||||
|
||||
Please proceed with helping resolve this issue following this exact workflow.
|
||||
|
||||
settings_json: |
|
||||
settings: |
|
||||
{
|
||||
"coreTools": [
|
||||
"run_shell_command(echo)",
|
||||
@@ -193,10 +196,5 @@ jobs:
|
||||
"web_fetch",
|
||||
"google_web_search",
|
||||
"save_memory"
|
||||
],
|
||||
"telemetry": {
|
||||
"enabled": false,
|
||||
"otlpGcpWifProvider": "${{ secrets.OTLP_GCP_WIF_PROVIDER }}",
|
||||
"googleCloudProject": "${{ secrets.OTLP_GOOGLE_CLOUD_PROJECT }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
name: Gemini PR Review
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
# issue_comment:
|
||||
# types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
@@ -159,6 +159,8 @@ jobs:
|
||||
- Analyze the code changes thoroughly
|
||||
|
||||
**STEP 2: If Code Improvements Are Needed**
|
||||
MANDATORY FIRST STEP: Before writing any code, you MUST use `read_file` to carefully read "src/praisonai-agents/AGENTS.md". You must honor this repository's extremely specific Architecture, Protocol-First design rules, and Agent-Centric Philosophy!
|
||||
|
||||
**IMPORTANT:** DO NOT create a new branch! Work on the EXISTING PR branch.
|
||||
- Use: `git fetch origin` to get latest changes
|
||||
- Get PR branch name: `gh pr view ${PR_NUMBER} --json headRefName`
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
|
||||
|
||||
name: Python package
|
||||
name: Python Package (Multi-Version)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "develop" ]
|
||||
pull_request:
|
||||
branches: [ "develop" ]
|
||||
schedule:
|
||||
- cron: '0 4 * * 1' # Weekly Monday 4am UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11"]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -30,39 +26,16 @@ jobs:
|
||||
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_MODEL_NAME=${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}" >> $GITHUB_ENV
|
||||
echo "LOGLEVEL=DEBUG" >> $GITHUB_ENV
|
||||
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/praisonai
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install flake8 pytest
|
||||
python -m pip install ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
python -m pip install duckduckgo_search
|
||||
python -m pip install pytest-asyncio pytest-cov
|
||||
# Install knowledge dependencies from praisonai-agents
|
||||
python -m pip install duckduckgo_search pytest pytest-asyncio pytest-cov
|
||||
python -m pip install "praisonaiagents[knowledge]"
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
|
||||
- name: Debug API Key Status
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
echo "🔍 Checking API key availability..."
|
||||
if [ -n "${{ secrets.OPENAI_API_KEY }}" ]; then
|
||||
echo "✅ GitHub secret OPENAI_API_KEY is available"
|
||||
echo "🔑 API key starts with: $(echo "$OPENAI_API_KEY" | cut -c1-7)..."
|
||||
else
|
||||
echo "⚠️ GitHub secret OPENAI_API_KEY is NOT set - using fallback"
|
||||
echo "🔑 Using fallback key: sk-test-key..."
|
||||
fi
|
||||
echo "🌐 API Base: $OPENAI_API_BASE"
|
||||
echo "🤖 Model: $OPENAI_MODEL_NAME"
|
||||
|
||||
# - name: Lint with flake8
|
||||
# run: |
|
||||
# # stop the build if there are Python syntax errors or undefined names
|
||||
# flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
cd src/praisonai && python tests/test_runner.py --pattern fast
|
||||
cd src/praisonai && python -m pytest tests/unit/ -v --tb=short --disable-warnings --timeout=60 || echo "Some tests failed"
|
||||
@@ -51,8 +51,8 @@ jobs:
|
||||
# Sync with upstream commit
|
||||
- name: Sync with upstream
|
||||
run: |
|
||||
git config --global user.name "GitHub Actions"
|
||||
git config --global user.email "actions@github.com"
|
||||
git config --global user.name "MervinPraison"
|
||||
git config --global user.email "[email protected].github.com"
|
||||
|
||||
# Add upstream remote
|
||||
git remote add upstream https://github.com/MervinPraison/PraisonAI.git
|
||||
@@ -473,8 +473,8 @@ jobs:
|
||||
# Create tag at the same commit
|
||||
- name: Create tag at commit
|
||||
run: |
|
||||
git config --global user.name "GitHub Actions"
|
||||
git config --global user.email "actions@github.com"
|
||||
git config --global user.name "MervinPraison"
|
||||
git config --global user.email "[email protected].github.com"
|
||||
|
||||
# Remove tag if it exists locally
|
||||
git tag -d ${{ github.event.inputs.tag }} || true
|
||||
|
||||
@@ -1,41 +1,44 @@
|
||||
name: Core Tests
|
||||
|
||||
on:
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/test-core.yml'
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/test-core.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test-core:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.11]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
timeout-minutes: 20
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install UV
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/praisonai
|
||||
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
uv pip install --system duckduckgo_search
|
||||
uv pip install --system pytest pytest-asyncio pytest-cov pytest-timeout
|
||||
# Install knowledge dependencies from praisonai-agents
|
||||
uv pip install --system "praisonaiagents[knowledge]"
|
||||
|
||||
- name: Set environment variables
|
||||
@@ -43,246 +46,28 @@ jobs:
|
||||
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_MODEL_NAME=${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}" >> $GITHUB_ENV
|
||||
echo "LOGLEVEL=DEBUG" >> $GITHUB_ENV
|
||||
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
|
||||
# Also export to current shell session for immediate availability
|
||||
export OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}"
|
||||
export OPENAI_API_BASE="${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}"
|
||||
export OPENAI_MODEL_NAME="${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}"
|
||||
export LOGLEVEL=DEBUG
|
||||
# Verify immediate availability
|
||||
echo "🔧 Immediate verification in same step:"
|
||||
echo " OPENAI_API_KEY length in current session: ${#OPENAI_API_KEY}"
|
||||
echo " OPENAI_API_KEY starts with sk-: $(echo "$OPENAI_API_KEY" | grep -q '^sk-' && echo 'YES' || echo 'NO')"
|
||||
|
||||
- name: Debug API Key Status
|
||||
run: |
|
||||
echo "🔍 Checking API key availability..."
|
||||
if [ -n "${{ secrets.OPENAI_API_KEY }}" ]; then
|
||||
echo "✅ GitHub secret OPENAI_API_KEY is available"
|
||||
echo "🔑 API key starts with: $(echo "$OPENAI_API_KEY" | cut -c1-7)..."
|
||||
else
|
||||
echo "⚠️ GitHub secret OPENAI_API_KEY is NOT set - using fallback"
|
||||
echo "🔑 Using fallback key: sk-test-key..."
|
||||
fi
|
||||
echo "🌐 API Base: $OPENAI_API_BASE"
|
||||
echo "🤖 Model: $OPENAI_MODEL_NAME"
|
||||
echo "🐛 Log Level: $LOGLEVEL"
|
||||
echo "📊 Environment Check:"
|
||||
echo " - OPENAI_API_KEY length: ${#OPENAI_API_KEY}"
|
||||
echo " - OPENAI_API_BASE: $OPENAI_API_BASE"
|
||||
echo " - OPENAI_MODEL_NAME: $OPENAI_MODEL_NAME"
|
||||
echo " - LOGLEVEL: $LOGLEVEL"
|
||||
echo "🔍 Is API key actually set?"
|
||||
echo " - API key starts with sk-: $(echo "$OPENAI_API_KEY" | grep -q '^sk-' && echo 'YES' || echo 'NO')"
|
||||
echo " - API key is not test key: $([ "$OPENAI_API_KEY" != 'sk-test-key-for-github-actions-testing-only-not-real' ] && echo 'YES' || echo 'NO')"
|
||||
|
||||
- name: Debug Environment Variables Raw
|
||||
run: |
|
||||
echo "🔧 Raw environment variable check:"
|
||||
echo "OPENAI_API_KEY set: $(if [ -n "$OPENAI_API_KEY" ]; then echo 'YES'; else echo 'NO'; fi)"
|
||||
echo "OPENAI_API_KEY length: ${#OPENAI_API_KEY}"
|
||||
echo "OPENAI_API_KEY first 10 chars: ${OPENAI_API_KEY:0:10}"
|
||||
echo "OPENAI_API_KEY last 5 chars: ${OPENAI_API_KEY: -5}"
|
||||
printenv | grep OPENAI || echo "No OPENAI env vars found"
|
||||
|
||||
- name: Debug Python Environment Variables
|
||||
run: |
|
||||
python -c "
|
||||
import os
|
||||
print('🐍 Python Environment Variable Check:')
|
||||
api_key = os.environ.get('OPENAI_API_KEY', 'NOT_SET')
|
||||
if api_key != 'NOT_SET':
|
||||
print(f' ✅ OPENAI_API_KEY: {api_key[:7]}... (length: {len(api_key)})')
|
||||
else:
|
||||
print(' ❌ OPENAI_API_KEY: NOT_SET')
|
||||
print(f' 🌐 OPENAI_API_BASE: {os.environ.get(\"OPENAI_API_BASE\", \"NOT_SET\")}')
|
||||
print(f' 🤖 OPENAI_MODEL_NAME: {os.environ.get(\"OPENAI_MODEL_NAME\", \"NOT_SET\")}')
|
||||
print(f' 📋 All OPENAI env vars:')
|
||||
for key, value in os.environ.items():
|
||||
if key.startswith('OPENAI'):
|
||||
print(f' {key}: {value[:10] if len(value) > 10 else value}...')
|
||||
"
|
||||
|
||||
- name: Validate API Key
|
||||
run: |
|
||||
echo "🔑 Testing API key validity with minimal OpenAI call..."
|
||||
python -c "
|
||||
import os
|
||||
try:
|
||||
from openai import OpenAI
|
||||
client = OpenAI(api_key=os.environ.get('OPENAI_API_KEY'))
|
||||
response = client.models.list()
|
||||
print('✅ API Key is VALID - OpenAI responded successfully')
|
||||
print(f'📊 Available models: {len(list(response.data))} models found')
|
||||
except Exception as e:
|
||||
print(f'❌ API Key is INVALID - Error: {e}')
|
||||
print('🔍 This explains why all API-dependent tests are failing')
|
||||
print('💡 The GitHub secret OPENAI_API_KEY needs to be updated with a valid key')
|
||||
"
|
||||
continue-on-error: true
|
||||
|
||||
- name: Debug PraisonAI API Key Usage
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}
|
||||
OPENAI_API_BASE: ${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}
|
||||
OPENAI_MODEL_NAME: ${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}
|
||||
LOGLEVEL: DEBUG
|
||||
run: |
|
||||
echo "🔍 Testing PraisonAI API key usage directly..."
|
||||
cd src/praisonai
|
||||
python -c "
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
# Attempt to import SecretStr, otherwise use a dummy class
|
||||
try:
|
||||
from pydantic.types import SecretStr
|
||||
except ImportError:
|
||||
class SecretStr: # Dummy class if pydantic is not available in this minimal context
|
||||
def __init__(self, value): self._value = value
|
||||
def get_secret_value(self): return self._value
|
||||
|
||||
def get_key_display_value(key_value):
|
||||
if isinstance(key_value, SecretStr):
|
||||
return key_value.get_secret_value()[:10] if key_value.get_secret_value() else 'EMPTY_SECRET'
|
||||
elif isinstance(key_value, str):
|
||||
return key_value[:10] if key_value != 'nokey' and key_value != 'NOT_SET' else key_value
|
||||
return 'INVALID_TYPE'
|
||||
|
||||
print('🔧 Direct PraisonAI API Key Check:')
|
||||
env_api_key = os.environ.get(\"OPENAI_API_KEY\", \"NOT_SET\")
|
||||
print(f'Environment OPENAI_API_KEY: {get_key_display_value(env_api_key)}...')
|
||||
|
||||
from praisonai import PraisonAI
|
||||
praisonai = PraisonAI()
|
||||
|
||||
print(f'PraisonAI config_list: {praisonai.config_list}')
|
||||
api_key_from_config = praisonai.config_list[0].get('api_key', 'NOT_SET')
|
||||
print(f'API key from PraisonAI config: {get_key_display_value(api_key_from_config)}...')
|
||||
|
||||
from praisonai.inc.models import PraisonAIModel
|
||||
|
||||
print('\\n🧪 Testing PraisonAIModel with explicit API key (CrewAI method):')
|
||||
model_with_explicit_key = PraisonAIModel(
|
||||
model='openai/gpt-5-nano',
|
||||
base_url=praisonai.config_list[0].get('base_url'),
|
||||
api_key=api_key_from_config # This will be a string from praisonai.config_list
|
||||
)
|
||||
print(f' Model: {model_with_explicit_key.model}')
|
||||
print(f' Model name: {model_with_explicit_key.model_name}')
|
||||
print(f' API key var: {model_with_explicit_key.api_key_var}')
|
||||
# model_with_explicit_key.api_key is now a string, or 'nokey'
|
||||
print(f' API key (explicitly passed to PraisonAIModel): {get_key_display_value(model_with_explicit_key.api_key)}...')
|
||||
print(f' Base URL: {model_with_explicit_key.base_url}')
|
||||
|
||||
try:
|
||||
llm_instance = model_with_explicit_key.get_model()
|
||||
print(f' ✅ LLM instance created successfully: {type(llm_instance).__name__}')
|
||||
|
||||
# langchain_openai.ChatOpenAI stores the key in openai_api_key as SecretStr
|
||||
llm_api_key_attr = getattr(llm_instance, 'openai_api_key', 'NOT_FOUND')
|
||||
if llm_api_key_attr != 'NOT_FOUND':
|
||||
print(f' LLM instance API key: {get_key_display_value(llm_api_key_attr)}...')
|
||||
else:
|
||||
print(f' LLM instance API key attribute not found.')
|
||||
except Exception as e:
|
||||
print(f' ❌ Failed to create LLM instance: {e}')
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
"
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: |
|
||||
cd src/praisonai && python -m pytest tests/unit/ -v --tb=short --disable-warnings --cov=praisonai --cov-report=term-missing --cov-report=xml --cov-branch || echo "Some tests failed - see output above"
|
||||
cd src/praisonai && python -m pytest tests/unit/ -v --tb=short --disable-warnings \
|
||||
--cov=praisonai --cov-report=xml --cov-branch \
|
||||
--timeout=60 || echo "Some unit tests failed"
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Integration Tests
|
||||
- name: Run Integration Tests
|
||||
run: |
|
||||
cd src/praisonai && python -m pytest tests/integration/ -v --tb=short --disable-warnings
|
||||
cd src/praisonai && python -m pytest tests/integration/ -v --tb=short --disable-warnings \
|
||||
--timeout=60 \
|
||||
--ignore=tests/integration/test_tracker_complex_tasks.py \
|
||||
--ignore=tests/integration/test_serve_integration.py \
|
||||
|| echo "Some integration tests failed"
|
||||
|
||||
- name: Debug Directory Structure
|
||||
run: |
|
||||
echo "🔍 Debugging directory structure for CrewAI tests..."
|
||||
cd src/praisonai
|
||||
echo "Current working directory: $(pwd)"
|
||||
echo "📁 Contents of current directory:"
|
||||
ls -la
|
||||
echo ""
|
||||
echo "📁 Contents of tests directory:"
|
||||
ls -la tests/ || echo "❌ tests/ directory not found"
|
||||
echo ""
|
||||
echo "📁 Contents of tests/integration:"
|
||||
ls -la tests/integration/ || echo "❌ tests/integration/ directory not found"
|
||||
echo ""
|
||||
echo "📁 Looking for crewai directory:"
|
||||
find . -name "crewai" -type d 2>/dev/null || echo "❌ No crewai directories found"
|
||||
echo ""
|
||||
echo "📁 Full directory tree of tests:"
|
||||
tree tests/ || find tests/ -type d 2>/dev/null || echo "❌ Cannot explore tests directory"
|
||||
|
||||
- name: Test AutoGen Framework
|
||||
run: |
|
||||
echo "🤖 Testing AutoGen Framework Integration..."
|
||||
cd src/praisonai && python tests/test_runner.py --pattern autogen --verbose
|
||||
|
||||
- name: Test CrewAI Framework
|
||||
run: |
|
||||
echo "⛵ Testing CrewAI Framework Integration..."
|
||||
cd src/praisonai
|
||||
echo "🔍 Trying test runner first..."
|
||||
python tests/test_runner.py --pattern crewai --verbose || {
|
||||
echo "❌ Test runner failed, trying direct pytest..."
|
||||
echo "📁 Current directory: $(pwd)"
|
||||
echo "📁 Looking for CrewAI tests..."
|
||||
find . -name "*crewai*" -type f 2>/dev/null
|
||||
echo "🧪 Trying direct pytest on integration/crewai..."
|
||||
python -m pytest tests/integration/crewai/ -v --tb=short --disable-warnings || {
|
||||
echo "❌ Direct path failed, trying relative path..."
|
||||
python -m pytest integration/crewai/ -v --tb=short --disable-warnings || {
|
||||
echo "❌ All CrewAI test attempts failed"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- name: Run Legacy Tests with API Key
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}
|
||||
OPENAI_API_BASE: ${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}
|
||||
OPENAI_MODEL_NAME: ${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}
|
||||
LOGLEVEL: DEBUG
|
||||
run: |
|
||||
echo "🧪 Running legacy tests with real API key..."
|
||||
echo "🔧 Final environment check before pytest:"
|
||||
echo " OPENAI_API_KEY set: $([ -n "$OPENAI_API_KEY" ] && echo 'YES' || echo 'NO')"
|
||||
echo " OPENAI_API_KEY length: ${#OPENAI_API_KEY}"
|
||||
echo " OPENAI_API_KEY starts with sk-: $(echo "$OPENAI_API_KEY" | grep -q '^sk-' && echo 'YES' || echo 'NO')"
|
||||
export OPENAI_API_KEY="$OPENAI_API_KEY"
|
||||
export OPENAI_API_BASE="$OPENAI_API_BASE"
|
||||
export OPENAI_MODEL_NAME="$OPENAI_MODEL_NAME"
|
||||
cd src/praisonai && python -m pytest tests/test.py -v --tb=short --disable-warnings -m "not real" --timeout=60
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: matrix.python-version == '3.11'
|
||||
- name: Upload coverage to Codecov
|
||||
if: always()
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: src/praisonai/coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN || '' }}
|
||||
files: src/praisonai/coverage.xml
|
||||
flags: core-tests
|
||||
name: core-tests-coverage
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
- name: Upload Coverage Reports (Artifacts)
|
||||
if: matrix.python-version == '3.11'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-reports
|
||||
path: |
|
||||
src/praisonai/.coverage
|
||||
src/praisonai/htmlcov/
|
||||
src/praisonai/coverage.xml
|
||||
retention-days: 7
|
||||
fail_ci_if_error: false
|
||||
@@ -1,14 +1,9 @@
|
||||
name: Extended Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
# Run nightly at 2 AM UTC
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test-examples:
|
||||
|
||||
@@ -3,8 +3,16 @@ name: Optimized Test Suite
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/test-optimized.yml'
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/test-optimized.yml'
|
||||
schedule:
|
||||
# Nightly extended tests at 3 AM UTC
|
||||
- cron: '0 3 * * *'
|
||||
@@ -149,8 +157,8 @@ jobs:
|
||||
if: matrix.python-version == '3.11'
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: src/praisonai/coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN || '' }}
|
||||
files: src/praisonai/coverage.xml
|
||||
flags: main-tests
|
||||
fail_ci_if_error: false
|
||||
|
||||
@@ -293,8 +301,10 @@ jobs:
|
||||
steps:
|
||||
- name: Check if Google key is available
|
||||
id: check-key
|
||||
env:
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
run: |
|
||||
if [ -n "${{ secrets.GOOGLE_API_KEY }}" ]; then
|
||||
if [ -n "$GOOGLE_API_KEY" ]; then
|
||||
echo "has_key=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_key=false" >> $GITHUB_OUTPUT
|
||||
@@ -348,8 +358,10 @@ jobs:
|
||||
steps:
|
||||
- name: Check if Groq key is available
|
||||
id: check-key
|
||||
env:
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
run: |
|
||||
if [ -n "${{ secrets.GROQ_API_KEY }}" ]; then
|
||||
if [ -n "$GROQ_API_KEY" ]; then
|
||||
echo "has_key=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_key=false" >> $GITHUB_OUTPUT
|
||||
@@ -403,8 +415,10 @@ jobs:
|
||||
steps:
|
||||
- name: Check if xAI key is available
|
||||
id: check-key
|
||||
env:
|
||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||
run: |
|
||||
if [ -n "${{ secrets.XAI_API_KEY }}" ]; then
|
||||
if [ -n "$XAI_API_KEY" ]; then
|
||||
echo "has_key=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_key=false" >> $GITHUB_OUTPUT
|
||||
@@ -458,8 +472,10 @@ jobs:
|
||||
steps:
|
||||
- name: Check if Cohere key is available
|
||||
id: check-key
|
||||
env:
|
||||
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
|
||||
run: |
|
||||
if [ -n "${{ secrets.COHERE_API_KEY }}" ]; then
|
||||
if [ -n "$COHERE_API_KEY" ]; then
|
||||
echo "has_key=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_key=false" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -3,8 +3,16 @@ name: Windows Tests
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/test-windows.yml'
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/test-windows.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -14,6 +22,7 @@ jobs:
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_MODEL_NAME: gpt-4o-mini
|
||||
PYTHONUTF8: 1
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -27,13 +36,13 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
cd src/praisonai-agents
|
||||
pip install .
|
||||
pip install -e .
|
||||
shell: bash
|
||||
|
||||
- name: Install praisonai wrapper
|
||||
run: |
|
||||
cd src/praisonai
|
||||
pip install .[dev]
|
||||
pip install -e .
|
||||
shell: bash
|
||||
|
||||
# ==================== CORE IMPORTS ====================
|
||||
@@ -41,7 +50,7 @@ jobs:
|
||||
run: |
|
||||
python -c "from praisonaiagents import Agent, Agents, Workflow; print('Core imports OK')"
|
||||
python -c "from praisonaiagents import tool, Tools; print('Tool imports OK')"
|
||||
python -c "from praisonaiagents import Task, WorkflowStep; print('Task/Step imports OK')"
|
||||
python -c "from praisonaiagents import Task; print('Task imports OK')"
|
||||
|
||||
# ==================== AGENT FEATURES ====================
|
||||
- name: Test Agent instantiation
|
||||
@@ -66,21 +75,13 @@ jobs:
|
||||
"
|
||||
|
||||
# ==================== WORKFLOW FEATURES ====================
|
||||
- name: Test Workflow and WorkflowStep
|
||||
- name: Test Workflow
|
||||
run: |
|
||||
python -c "
|
||||
from praisonaiagents import Workflow, WorkflowStep
|
||||
from praisonaiagents.workflows import WorkflowOutputConfig
|
||||
from praisonaiagents import Workflow
|
||||
|
||||
w = Workflow(name='test')
|
||||
s1 = WorkflowStep(name='step1')
|
||||
s2 = WorkflowStep(name='step2', context=['step1'])
|
||||
print('Workflow:', w.name)
|
||||
print('Steps:', s1.name, s2.name)
|
||||
|
||||
cfg = WorkflowOutputConfig(verbose=True, stream=False)
|
||||
w2 = Workflow(name='configured', output=cfg)
|
||||
print('Configured workflow verbose:', w2.verbose)
|
||||
"
|
||||
|
||||
# ==================== MEMORY FEATURES ====================
|
||||
|
||||
@@ -1,516 +0,0 @@
|
||||
name: Quick Validation Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
quick-test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
- name: Install UV
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/praisonai
|
||||
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
|
||||
uv pip install --system duckduckgo_search
|
||||
uv pip install --system pytest pytest-asyncio pytest-cov pytest-timeout
|
||||
# Install knowledge dependencies from praisonai-agents
|
||||
uv pip install --system "praisonaiagents[knowledge]"
|
||||
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
|
||||
echo "OPENAI_MODEL_NAME=${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}" >> $GITHUB_ENV
|
||||
echo "LOGLEVEL=DEBUG" >> $GITHUB_ENV
|
||||
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
|
||||
|
||||
- name: Backup Root Config Files
|
||||
run: |
|
||||
echo "🔄 Backing up root configuration files to prevent default file resolution interference..."
|
||||
echo "ℹ️ PraisonAI automatically uses 'agents.yaml' in working directory when no file specified"
|
||||
echo "ℹ️ This can interfere with test files that specify their own YAML files"
|
||||
if [ -f "agents.yaml" ]; then
|
||||
mv agents.yaml agents.yaml.backup
|
||||
echo "✅ Moved root agents.yaml to agents.yaml.backup"
|
||||
echo " - This prevents default file fallback during tests"
|
||||
fi
|
||||
if [ -f "tools.py" ]; then
|
||||
mv tools.py tools.py.backup
|
||||
echo "✅ Moved tools.py to tools.py.backup"
|
||||
fi
|
||||
|
||||
- name: Debug API Key Status
|
||||
run: |
|
||||
echo "🔍 Checking API key availability..."
|
||||
if [ -n "${{ secrets.OPENAI_API_KEY }}" ]; then
|
||||
echo "✅ GitHub secret OPENAI_API_KEY is available"
|
||||
echo "🔑 API key starts with: $(echo "$OPENAI_API_KEY" | cut -c1-7)..."
|
||||
else
|
||||
echo "⚠️ GitHub secret OPENAI_API_KEY is NOT set - using fallback"
|
||||
echo "🔑 Using fallback key: sk-test-key..."
|
||||
fi
|
||||
echo "🌐 API Base: $OPENAI_API_BASE"
|
||||
echo "🤖 Model: $OPENAI_MODEL_NAME"
|
||||
echo "🐛 Log Level: $LOGLEVEL"
|
||||
echo "📊 Environment Check:"
|
||||
echo " - OPENAI_API_KEY length: ${#OPENAI_API_KEY}"
|
||||
echo " - OPENAI_API_BASE: $OPENAI_API_BASE"
|
||||
echo " - OPENAI_MODEL_NAME: $OPENAI_MODEL_NAME"
|
||||
echo " - LOGLEVEL: $LOGLEVEL"
|
||||
|
||||
- name: Debug Python Environment Variables
|
||||
run: |
|
||||
python -c "
|
||||
import os
|
||||
print('🐍 Python Environment Variable Check:')
|
||||
api_key = os.environ.get('OPENAI_API_KEY', 'NOT_SET')
|
||||
if api_key != 'NOT_SET':
|
||||
print(f' ✅ OPENAI_API_KEY: {api_key[:7]}... (length: {len(api_key)})')
|
||||
else:
|
||||
print(' ❌ OPENAI_API_KEY: NOT_SET')
|
||||
print(f' 🌐 OPENAI_API_BASE: {os.environ.get(\"OPENAI_API_BASE\", \"NOT_SET\")}')
|
||||
print(f' 🤖 OPENAI_MODEL_NAME: {os.environ.get(\"OPENAI_MODEL_NAME\", \"NOT_SET\")}')
|
||||
print(f' 📋 All OPENAI env vars:')
|
||||
for key, value in os.environ.items():
|
||||
if key.startswith('OPENAI'):
|
||||
print(f' {key}: {value[:10] if len(value) > 10 else value}...')
|
||||
"
|
||||
|
||||
- name: Find Researcher Role Source
|
||||
run: |
|
||||
echo "🔍 Hunting for the mysterious 'Researcher' role..."
|
||||
cd src/praisonai
|
||||
python -c "
|
||||
import os
|
||||
import yaml
|
||||
import glob
|
||||
|
||||
print('📋 Searching for Researcher role in all YAML files:')
|
||||
yaml_files = glob.glob('src/praisonai/tests/*.yaml')
|
||||
|
||||
for yaml_file in yaml_files:
|
||||
try:
|
||||
with open(yaml_file, 'r') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# Check if any role contains 'researcher'
|
||||
roles = config.get('roles', {})
|
||||
for role_key, role_data in roles.items():
|
||||
role_name = role_data.get('role', '')
|
||||
if 'researcher' in role_key.lower() or 'researcher' in role_name.lower():
|
||||
print(f' 🎯 FOUND in {yaml_file}:')
|
||||
print(f' Framework: {config.get(\"framework\", \"NOT_SET\")}')
|
||||
print(f' Role key: {role_key}')
|
||||
print(f' Role name: {role_name}')
|
||||
print(f' All roles: {list(roles.keys())}')
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f' ❌ Error reading {yaml_file}: {e}')
|
||||
|
||||
print('🔍 Checking for default configurations...')
|
||||
# Check if there are any default configs or hardcoded roles
|
||||
try:
|
||||
import praisonai
|
||||
print(f' PraisonAI package location: {praisonai.__file__}')
|
||||
|
||||
# Check if there are any example YAML files in the package
|
||||
package_dir = os.path.dirname(praisonai.__file__)
|
||||
for root, dirs, files in os.walk(package_dir):
|
||||
for file in files:
|
||||
if file.endswith(('.yaml', '.yml')):
|
||||
file_path = os.path.join(root, file)
|
||||
print(f' 📁 Found YAML in package: {file_path}')
|
||||
except Exception as e:
|
||||
print(f' ❌ Error checking package: {e}')
|
||||
"
|
||||
continue-on-error: true
|
||||
|
||||
- name: Trace AutoGen Execution Path
|
||||
run: |
|
||||
echo "🔍 Tracing AutoGen execution to find where it diverges..."
|
||||
cd src/praisonai
|
||||
python -c "
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
try:
|
||||
from praisonai import PraisonAI
|
||||
from praisonai.agents_generator import AgentsGenerator
|
||||
|
||||
# Test the exact execution path
|
||||
print('🎯 Testing AutoGen execution path:')
|
||||
|
||||
praisonai = PraisonAI(agent_file='tests/autogen-agents.yaml')
|
||||
print(f' 1. PraisonAI framework: \"{praisonai.framework}\"')
|
||||
|
||||
agents_gen = AgentsGenerator(
|
||||
agent_file='tests/autogen-agents.yaml',
|
||||
framework=praisonai.framework,
|
||||
config_list=praisonai.config_list
|
||||
)
|
||||
print(f' 2. AgentsGenerator framework: \"{agents_gen.framework}\"')
|
||||
|
||||
# Load the YAML to check what it contains
|
||||
import yaml
|
||||
with open('src/praisonai/tests/autogen-agents.yaml', 'r') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
framework = agents_gen.framework or config.get('framework')
|
||||
print(f' 3. Final framework decision: \"{framework}\"')
|
||||
print(f' 4. Available frameworks:')
|
||||
|
||||
# Check framework availability
|
||||
try:
|
||||
import autogen
|
||||
print(f' ✅ AutoGen available')
|
||||
except ImportError:
|
||||
print(f' ❌ AutoGen NOT available')
|
||||
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
print(f' ✅ PraisonAI agents available')
|
||||
except ImportError:
|
||||
print(f' ❌ PraisonAI agents NOT available')
|
||||
|
||||
try:
|
||||
from crewai import Agent
|
||||
print(f' ✅ CrewAI available')
|
||||
except ImportError:
|
||||
print(f' ❌ CrewAI NOT available')
|
||||
|
||||
print(f' 5. Roles in YAML: {list(config.get(\"roles\", {}).keys())}')
|
||||
|
||||
# Now test the actual framework execution
|
||||
if framework == 'autogen':
|
||||
print(f' 6. ✅ Should execute _run_autogen')
|
||||
elif framework == 'praisonai':
|
||||
print(f' 6. ❌ Would execute _run_praisonai (WRONG!)')
|
||||
else:
|
||||
print(f' 6. ❌ Would execute _run_crewai (DEFAULT FALLBACK)')
|
||||
|
||||
except Exception as e:
|
||||
print(f'❌ Error tracing execution: {e}')
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
"
|
||||
continue-on-error: true
|
||||
|
||||
- name: Debug YAML Loading and Roles
|
||||
run: |
|
||||
echo "🔍 Tracing YAML file loading and role creation..."
|
||||
cd src/praisonai
|
||||
python -c "
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
print('📁 Available YAML files in src/praisonai/tests/:')
|
||||
import glob
|
||||
yaml_files = glob.glob('tests/*.yaml')
|
||||
for f in yaml_files:
|
||||
print(f' {f}')
|
||||
|
||||
print()
|
||||
print('📋 Content of autogen-agents.yaml:')
|
||||
with open('tests/autogen-agents.yaml', 'r') as f:
|
||||
config = yaml.safe_load(f)
|
||||
print(f' Framework: {config.get(\"framework\")}')
|
||||
print(f' Topic: {config.get(\"topic\")}')
|
||||
print(f' Roles: {list(config.get(\"roles\", {}).keys())}')
|
||||
for role_key, role_data in config.get('roles', {}).items():
|
||||
print(f' {role_key} -> {role_data.get(\"role\", \"NO_ROLE\")}')
|
||||
|
||||
print()
|
||||
print('🔍 Checking if execution uses a different YAML:')
|
||||
|
||||
# Check other YAML files for 'Researcher' role
|
||||
for yaml_file in yaml_files:
|
||||
try:
|
||||
with open(yaml_file, 'r') as f:
|
||||
test_config = yaml.safe_load(f)
|
||||
roles = test_config.get('roles', {})
|
||||
for role_key, role_data in roles.items():
|
||||
if 'researcher' in role_key.lower() or 'researcher' in role_data.get('role', '').lower():
|
||||
print(f' 🎯 FOUND Researcher in {yaml_file}!')
|
||||
print(f' Framework: {test_config.get(\"framework\")}')
|
||||
print(f' Role key: {role_key} -> {role_data.get(\"role\")}')
|
||||
except:
|
||||
pass
|
||||
"
|
||||
continue-on-error: true
|
||||
|
||||
- name: Debug Framework Detection
|
||||
run: |
|
||||
echo "🔍 Testing framework detection and config flow..."
|
||||
python -c "
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
print('🔧 Testing framework detection:')
|
||||
|
||||
# Load the YAML file
|
||||
with open('tests/autogen-agents.yaml', 'r') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
print(f' 📋 YAML framework: {config.get(\"framework\", \"NOT_SET\")}')
|
||||
print(f' 📋 YAML topic: {config.get(\"topic\", \"NOT_SET\")}')
|
||||
|
||||
try:
|
||||
from praisonai import PraisonAI
|
||||
from praisonai.agents_generator import AgentsGenerator
|
||||
|
||||
# Test PraisonAI initialization
|
||||
praisonai = PraisonAI(agent_file='tests/autogen-agents.yaml')
|
||||
print(f' 🎯 PraisonAI framework: {praisonai.framework}')
|
||||
|
||||
# Test AgentsGenerator initialization
|
||||
agents_gen = AgentsGenerator(
|
||||
agent_file='tests/autogen-agents.yaml',
|
||||
framework=praisonai.framework,
|
||||
config_list=praisonai.config_list
|
||||
)
|
||||
print(f' ⚙️ AgentsGenerator framework: {agents_gen.framework}')
|
||||
print(f' ⚙️ Final framework decision: {agents_gen.framework or config.get(\"framework\")}')
|
||||
|
||||
# Check config_list
|
||||
print(f' 🔑 Config list model: {praisonai.config_list[0].get(\"model\")}')
|
||||
print(f' 🔑 Config list API key: {praisonai.config_list[0].get(\"api_key\", \"NOT_SET\")[:10]}...')
|
||||
|
||||
except Exception as e:
|
||||
print(f'❌ Error in framework detection: {e}')
|
||||
"
|
||||
continue-on-error: true
|
||||
|
||||
- name: Debug PraisonAIModel API Key Flow
|
||||
run: |
|
||||
echo "🔍 Testing PraisonAIModel API key handling..."
|
||||
cd src/praisonai
|
||||
python -c "
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
print('🔑 Environment API Key Check:')
|
||||
env_key = os.environ.get('OPENAI_API_KEY', 'NOT_FOUND')
|
||||
print(f' OPENAI_API_KEY: {env_key[:10] if env_key != \"NOT_FOUND\" else \"NOT_FOUND\"}...')
|
||||
|
||||
try:
|
||||
from praisonai.inc.models import PraisonAIModel
|
||||
|
||||
# Test PraisonAIModel with openai/gpt-5-nano (from YAML)
|
||||
model = PraisonAIModel(model='openai/gpt-5-nano')
|
||||
|
||||
print('🤖 PraisonAIModel Configuration:')
|
||||
print(f' model: {model.model}')
|
||||
print(f' model_name: {model.model_name}')
|
||||
print(f' api_key_var: {model.api_key_var}')
|
||||
print(f' api_key: {model.api_key[:10] if model.api_key != \"nokey\" else \"DEFAULT_NOKEY\"}...')
|
||||
print(f' base_url: {model.base_url}')
|
||||
|
||||
if model.api_key == 'nokey':
|
||||
print('❌ FOUND THE ISSUE: PraisonAIModel is using default \"nokey\" instead of environment variable!')
|
||||
else:
|
||||
print('✅ PraisonAIModel has valid API key from environment')
|
||||
|
||||
except Exception as e:
|
||||
print(f'❌ Error testing PraisonAIModel: {e}')
|
||||
"
|
||||
continue-on-error: true
|
||||
|
||||
- name: Validate API Key
|
||||
run: |
|
||||
echo "🔑 Testing API key validity with minimal OpenAI call..."
|
||||
python -c "
|
||||
import os
|
||||
try:
|
||||
from openai import OpenAI
|
||||
client = OpenAI(api_key=os.environ.get('OPENAI_API_KEY'))
|
||||
# Make a minimal API call to test key validity
|
||||
response = client.models.list()
|
||||
print('✅ API Key is VALID - OpenAI responded successfully')
|
||||
print(f'📊 Available models: {len(list(response.data))} models found')
|
||||
except Exception as e:
|
||||
print(f'❌ API Key is INVALID - Error: {e}')
|
||||
print('🔍 This explains why all API-dependent tests are failing')
|
||||
print('💡 The GitHub secret OPENAI_API_KEY needs to be updated with a valid key')
|
||||
"
|
||||
continue-on-error: true
|
||||
|
||||
- name: Test Direct PraisonAI Execution
|
||||
run: |
|
||||
echo "🧪 Testing direct PraisonAI execution (what works locally)..."
|
||||
python -m praisonai src/praisonai/tests/autogen-agents.yaml
|
||||
continue-on-error: true
|
||||
|
||||
- name: Comprehensive Execution Debug
|
||||
run: |
|
||||
echo "🔍 Comprehensive debugging of PraisonAI execution path..."
|
||||
cd src/praisonai
|
||||
python -c "
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
print('=' * 60)
|
||||
print('🔍 COMPREHENSIVE EXECUTION DEBUG')
|
||||
print('=' * 60)
|
||||
|
||||
# Check current working directory
|
||||
print(f'📁 Current working directory: {os.getcwd()}')
|
||||
|
||||
# List files in current directory
|
||||
print('📋 Files in current directory:')
|
||||
for f in os.listdir('.'):
|
||||
print(f' {f}')
|
||||
|
||||
print()
|
||||
print('📋 Files in src/praisonai/tests/ directory:')
|
||||
for f in os.listdir('tests'):
|
||||
if f.endswith('.yaml'):
|
||||
print(f' src/praisonai/tests/{f}')
|
||||
|
||||
# Check if root agents.yaml exists
|
||||
print()
|
||||
if os.path.exists('agents.yaml'):
|
||||
print('❌ ROOT agents.yaml EXISTS (this is the problem!)')
|
||||
with open('agents.yaml', 'r') as f:
|
||||
root_config = yaml.safe_load(f)
|
||||
print(f' Root framework: {root_config.get(\"framework\")}')
|
||||
print(f' Root roles: {list(root_config.get(\"roles\", {}).keys())}')
|
||||
else:
|
||||
print('✅ ROOT agents.yaml does NOT exist (good!)')
|
||||
|
||||
# Test the actual execution path
|
||||
print()
|
||||
print('🎯 Testing EXACT execution path:')
|
||||
|
||||
try:
|
||||
from praisonai import PraisonAI
|
||||
from praisonai.agents_generator import AgentsGenerator
|
||||
|
||||
# Test with full path
|
||||
test_file = 'src/praisonai/tests/autogen-agents.yaml'
|
||||
print(f' Using test file: {test_file}')
|
||||
print(f' File exists: {os.path.exists(test_file)}')
|
||||
|
||||
# Load the test file directly
|
||||
with open(test_file, 'r') as f:
|
||||
test_config = yaml.safe_load(f)
|
||||
print(f' Test file framework: {test_config.get(\"framework\")}')
|
||||
print(f' Test file roles: {list(test_config.get(\"roles\", {}).keys())}')
|
||||
|
||||
# Create PraisonAI instance
|
||||
praisonai = PraisonAI(agent_file=test_file)
|
||||
print(f' PraisonAI.agent_file: {praisonai.agent_file}')
|
||||
print(f' PraisonAI.framework: {praisonai.framework}')
|
||||
|
||||
# Create AgentsGenerator
|
||||
agents_gen = AgentsGenerator(
|
||||
agent_file=test_file,
|
||||
framework=praisonai.framework,
|
||||
config_list=praisonai.config_list
|
||||
)
|
||||
print(f' AgentsGenerator.agent_file: {agents_gen.agent_file}')
|
||||
print(f' AgentsGenerator.framework: {agents_gen.framework}')
|
||||
|
||||
# Test what would happen in generate_crew_and_kickoff
|
||||
print()
|
||||
print('🔧 Testing generate_crew_and_kickoff logic:')
|
||||
|
||||
# Simulate the loading logic
|
||||
if agents_gen.agent_yaml:
|
||||
loaded_config = yaml.safe_load(agents_gen.agent_yaml)
|
||||
print(' Would load from agent_yaml')
|
||||
else:
|
||||
if agents_gen.agent_file == '/app/api:app' or agents_gen.agent_file == 'api:app':
|
||||
agents_gen.agent_file = 'agents.yaml'
|
||||
print(f' Would change agent_file to: {agents_gen.agent_file}')
|
||||
try:
|
||||
with open(agents_gen.agent_file, 'r') as f:
|
||||
loaded_config = yaml.safe_load(f)
|
||||
print(f' Successfully loaded: {agents_gen.agent_file}')
|
||||
except FileNotFoundError:
|
||||
print(f' FileNotFoundError: {agents_gen.agent_file}')
|
||||
loaded_config = None
|
||||
|
||||
if loaded_config:
|
||||
final_framework = agents_gen.framework or loaded_config.get('framework')
|
||||
print(f' Final framework decision: {final_framework}')
|
||||
print(f' Loaded roles: {list(loaded_config.get(\"roles\", {}).keys())}')
|
||||
|
||||
if 'researcher' in loaded_config.get('roles', {}):
|
||||
print(' ❌ FOUND Researcher role in loaded config!')
|
||||
else:
|
||||
print(' ✅ No Researcher role in loaded config')
|
||||
|
||||
except Exception as e:
|
||||
print(f'❌ Error during execution debug: {e}')
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
"
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Fast Tests
|
||||
run: |
|
||||
# Run the fastest, most essential tests with coverage
|
||||
cd src/praisonai && python -m pytest tests/unit/test_core_agents.py -v --tb=short --disable-warnings --cov=praisonai --cov-report=xml --cov-branch
|
||||
|
||||
- name: Run Real API Tests
|
||||
run: |
|
||||
echo "🔑 Running real API tests with actual OpenAI API key..."
|
||||
cd src/praisonai && python -m pytest tests/test_agents_playbook.py -v --tb=short --disable-warnings -m real
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run E2E Real Tests
|
||||
run: |
|
||||
echo "🧪 Running E2E real tests..."
|
||||
cd src/praisonai && python -m pytest tests/e2e/ -v --tb=short --disable-warnings -m real
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Legacy Example Tests
|
||||
run: |
|
||||
cd src/praisonai && python -m pytest tests/test.py -v --tb=short --disable-warnings -m "not real" --timeout=60
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: src/praisonai/coverage.xml
|
||||
flags: quick-validation
|
||||
name: quick-validation-coverage
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
- name: Restore Root Config Files
|
||||
run: |
|
||||
echo "🔄 Restoring root configuration files..."
|
||||
if [ -f "agents.yaml.backup" ]; then
|
||||
mv agents.yaml.backup agents.yaml
|
||||
echo "✅ Restored agents.yaml"
|
||||
fi
|
||||
if [ -f "tools.py.backup" ]; then
|
||||
mv tools.py.backup tools.py
|
||||
echo "✅ Restored tools.py"
|
||||
fi
|
||||
if: always() # Always run this step, even if previous steps failed
|
||||
@@ -52,6 +52,6 @@ jobs:
|
||||
with:
|
||||
commit_message: "docs: auto-update api.md [skip ci]"
|
||||
file_pattern: 'api.md'
|
||||
commit_user_name: "github-actions[bot]"
|
||||
commit_user_email: "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
commit_author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
|
||||
commit_user_name: "MervinPraison"
|
||||
commit_user_email: "454862+MervinPraison@users.noreply.github.com"
|
||||
commit_author: "MervinPraison <454862+MervinPraison@users.noreply.github.com>"
|
||||
@@ -27,11 +27,13 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout PraisonAIDocs
|
||||
id: checkout_docs
|
||||
continue-on-error: true
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: MervinPraison/PraisonAIDocs
|
||||
path: PraisonAIDocs
|
||||
token: ${{ secrets.DOCS_PAT }}
|
||||
token: ${{ secrets.SECRETS_ADMIN_PAT }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -43,6 +45,7 @@ jobs:
|
||||
pip install pyyaml
|
||||
|
||||
- name: Generate docs parity trackers
|
||||
if: steps.checkout_docs.outcome == 'success'
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
python3 src/praisonai/scripts/generate_docs_parity.py --docs-root PraisonAIDocs/docs --copy-docs
|
||||
@@ -65,16 +68,16 @@ jobs:
|
||||
with:
|
||||
commit_message: "docs: auto-update documentation parity trackers [skip ci]"
|
||||
file_pattern: 'src/praisonai-agents/DOCS_PARITY.md src/praisonai-ts/DOCS_PARITY.md src/praisonai-rust/DOCS_PARITY.md'
|
||||
commit_user_name: "github-actions[bot]"
|
||||
commit_user_email: "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
commit_author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
|
||||
commit_user_name: "MervinPraison"
|
||||
commit_user_email: "454862+MervinPraison@users.noreply.github.com"
|
||||
commit_author: "MervinPraison <454862+MervinPraison@users.noreply.github.com>"
|
||||
|
||||
- name: Commit and push changes to PraisonAIDocs
|
||||
if: steps.check_changes.outputs.changes == 'true'
|
||||
if: steps.check_changes.outputs.changes == 'true' && steps.checkout_docs.outcome == 'success'
|
||||
run: |
|
||||
cd PraisonAIDocs
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config user.name "MervinPraison"
|
||||
git config user.email "454862+MervinPraison@users.noreply.github.com"
|
||||
git add docs/features/DOCS_PARITY.md docs/js/DOCS_PARITY.md docs/rust/DOCS_PARITY.md
|
||||
git diff --staged --quiet || git commit -m "docs: auto-update documentation parity trackers [skip ci]"
|
||||
git push
|
||||
@@ -29,6 +29,9 @@ jobs:
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install pyyaml
|
||||
|
||||
- name: Generate parity trackers
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
@@ -52,7 +55,7 @@ jobs:
|
||||
with:
|
||||
commit_message: "docs: auto-update feature parity trackers [skip ci]"
|
||||
file_pattern: 'src/praisonai-ts/FEATURE_PARITY_TRACKER.json src/praisonai-ts/PARITY.md src/praisonai-rust/FEATURE_PARITY_TRACKER.json src/praisonai-rust/PARITY.md'
|
||||
commit_user_name: "github-actions[bot]"
|
||||
commit_user_email: "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
commit_author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
|
||||
commit_user_name: "MervinPraison"
|
||||
commit_user_email: "454862+MervinPraison@users.noreply.github.com"
|
||||
commit_author: "MervinPraison <454862+MervinPraison@users.noreply.github.com>"
|
||||
|
||||
@@ -21,6 +21,7 @@ docs.sh
|
||||
other
|
||||
output
|
||||
!src/praisonai/praisonai/cli/output/
|
||||
!src/praisonai-agents/praisonaiagents/output/
|
||||
|
||||
.files
|
||||
threads.db
|
||||
@@ -95,4 +96,5 @@ Cargo.lock
|
||||
.windsurf
|
||||
.agent
|
||||
|
||||
src/praisonai/build
|
||||
src/praisonai/build*.egg-info/
|
||||
src/praisonai/PraisonAI.egg-info/
|
||||
@@ -1,8 +1,8 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="src/praisonai/praisonai/public/logo_dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="src/praisonai/praisonai/public/logo_light.png" />
|
||||
<img alt="PraisonAI Logo" src="src/praisonai/praisonai/public/logo_light.png" width="250" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset=".github/images/logo_dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset=".github/images/logo_light.png" />
|
||||
<img alt="PraisonAI Logo" src=".github/images/logo_light.png" width="250" />
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
@@ -25,10 +25,40 @@
|
||||
|
||||
PraisonAI 🦞 — Automate and solve complex challenges with AI agent teams that plan, research, code, and deliver results to Telegram, Discord, and WhatsApp — running 24/7. A low-code, production-ready multi-agent framework with handoffs, guardrails, memory, RAG, and 100+ LLM providers, built around simplicity, customisation, and effective human-agent collaboration.
|
||||
|
||||
<div align="center">
|
||||
<br>
|
||||
<a href="https://x.com/elonmusk/status/1893870468249141688" target="_blank">
|
||||
<img src="https://img.shields.io/badge/Highlighted_by_Elon_Musk-000000?style=for-the-badge&logo=x&logoColor=white" alt="Highlighted by Elon Musk" />
|
||||
</a>
|
||||
<p><em>"Grok 3 customer support" — <a href="https://x.com/elonmusk/status/1893870468249141688">Elon Musk quoting PraisonAI's tutorial</a></em></p>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<img src="src/praisonai/praisonai/public/dashboard.png" alt="PraisonAI Dashboard" width="800" />
|
||||
<img src=".github/images/dashboard.png" alt="PraisonAI Dashboard" width="800" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src=".github/images/agentflow.gif" alt="PraisonAI AgentFlow" width="800" />
|
||||
</p>
|
||||
|
||||
```
|
||||
██████╗ ██████╗ █████╗ ██╗███████╗ ██████╗ ███╗ ██╗ █████╗ ██╗
|
||||
██╔══██╗██╔══██╗██╔══██╗██║██╔════╝██╔═══██╗████╗ ██║ ██╔══██╗██║
|
||||
██████╔╝██████╔╝███████║██║███████╗██║ ██║██╔██╗ ██║ ███████║██║
|
||||
██╔═══╝ ██╔══██╗██╔══██║██║╚════██║██║ ██║██║╚██╗██║ ██╔══██║██║
|
||||
██║ ██║ ██║██║ ██║██║███████║╚██████╔╝██║ ╚████║ ██║ ██║██║
|
||||
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═╝
|
||||
|
||||
pip install praisonai
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<img src=".github/images/latest_ai_news_and_crawl_each_url_to_find_info.gif" alt="PraisonAI command execution" width="800" />
|
||||
</p>
|
||||
|
||||
\* `export TAVILY_API_KEY=xxxxx`
|
||||
|
||||
<div align="center">
|
||||
<a href="https://docs.praison.ai">
|
||||
<p align="center">
|
||||
@@ -43,8 +73,9 @@ PraisonAI 🦞 — Automate and solve complex challenges with AI agent teams tha
|
||||
> - 🆕 **New here?** → [Quick Start](#-quick-start) *(1 minute to first agent)*
|
||||
> - 📦 **Installing?** → [Installation](#-installation)
|
||||
> - 🐍 **Python SDK?** → [Python Examples](#-using-python-code)
|
||||
> - 🎯 **CLI user?** → [CLI Quick Reference](#cli-quick-reference)
|
||||
> - 🤝 **Contributing?** → [Development](#-development)
|
||||
> - 📄 **YAML/No-Code?** → [YAML Examples](#-using-yaml-no-code)
|
||||
> - 🎯 **CLI user?** → [CLI Quick Reference](#-cli-quick-reference)
|
||||
> - 🤝 **Contributing?** → [Contributing](#-contributing)
|
||||
|
||||
---
|
||||
|
||||
@@ -173,6 +204,15 @@ PraisonAI supports 100+ LLM providers through seamless integration:
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<div align="center">
|
||||
<a href="https://x.com/elonmusk/status/1893870468249141688" target="_blank">
|
||||
<img src=".github/images/elon_musk_praisonai.png" alt="Highlighted by Elon Musk" width="600" />
|
||||
</a>
|
||||
<p><em>"Grok 3 customer support" — <a href="https://x.com/elonmusk/status/1893870468249141688">Elon Musk quoting PraisonAI's tutorial</a></em></p>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
@@ -210,13 +250,27 @@ For the full framework with CLI support:
|
||||
pip install praisonai
|
||||
```
|
||||
|
||||
🦞 **AgentClaw** — full UI with bots, memory, knowledge, and gateway:
|
||||
🦞 **PraisonAI Claw** — full UI with bots, memory, knowledge, and gateway:
|
||||
|
||||
```bash
|
||||
pip install "praisonai[claw]"
|
||||
praisonai claw
|
||||
```
|
||||
|
||||
🔗 **PraisonAI Flow** — Langflow Visual Flow Builder:
|
||||
|
||||
```bash
|
||||
pip install "praisonai[flow]"
|
||||
praisonai flow
|
||||
```
|
||||
|
||||
🤖 **PraisonAI UI** — Clean chat interface:
|
||||
|
||||
```bash
|
||||
pip install "praisonai[ui]"
|
||||
praisonai ui
|
||||
```
|
||||
|
||||
### JavaScript SDK
|
||||
|
||||
```bash
|
||||
@@ -313,7 +367,7 @@ agent.chat("Hello!") # Auto-persists messages, runs, traces
|
||||
|
||||
> 📖 [Full persistence docs](https://docs.praison.ai/docs/databases/overview) — PostgreSQL, MySQL, SQLite, MongoDB, Redis, and 20+ more
|
||||
|
||||
### 6. AgentClaw 🦞 (Dashboard UI)
|
||||
### 6. PraisonAI Claw 🦞 (Dashboard UI)
|
||||
|
||||
Connect your AI agents to **Telegram, Discord, Slack, WhatsApp** and more — all from a single command.
|
||||
|
||||
@@ -326,6 +380,103 @@ Open **http://localhost:8082** — the dashboard comes with 13 built-in pages: C
|
||||
|
||||
> 📖 [Full Claw docs](https://docs.praison.ai/docs/concepts/claw) — platform tokens, CLI options, Docker, and YAML agent mode
|
||||
|
||||
### 7. Langflow Integration 🔗 (Visual Flow Builder)
|
||||
|
||||
Build multi-agent workflows visually with **drag-and-drop** components in Langflow.
|
||||
|
||||
```bash
|
||||
pip install "praisonai[flow]"
|
||||
praisonai flow
|
||||
```
|
||||
|
||||
Open **http://localhost:7861** — use the **Agent** and **Agent Team** components to create sequential or parallel workflows. Connect Chat Input → Agent Team → Chat Output for instant multi-agent pipelines.
|
||||
|
||||
> 📖 [Full Flow docs](https://docs.praison.ai/docs/concepts/flow) — visual agent building, component reference, and deployment
|
||||
|
||||
### 8. PraisonAI UI 🤖 (Clean Chat)
|
||||
|
||||
Lightweight chat interface for your AI agents.
|
||||
|
||||
```bash
|
||||
pip install "praisonai[ui]"
|
||||
praisonai ui
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 Using YAML (No Code)
|
||||
|
||||
### Example 1: Two Agents Working Together
|
||||
|
||||
Create `agents.yaml`:
|
||||
|
||||
```yaml
|
||||
framework: praisonai
|
||||
topic: "Write a blog post about AI"
|
||||
|
||||
agents:
|
||||
researcher:
|
||||
role: Research Analyst
|
||||
goal: Research AI trends and gather information
|
||||
instructions: "Find accurate information about AI trends"
|
||||
|
||||
writer:
|
||||
role: Content Writer
|
||||
goal: Write engaging blog posts
|
||||
instructions: "Write clear, engaging content based on research"
|
||||
```
|
||||
|
||||
Run with:
|
||||
```bash
|
||||
praisonai agents.yaml
|
||||
```
|
||||
|
||||
> The agents automatically work together sequentially
|
||||
|
||||
### Example 2: Agent with Custom Tool
|
||||
|
||||
Create two files in the same folder:
|
||||
|
||||
**agents.yaml:**
|
||||
```yaml
|
||||
framework: praisonai
|
||||
topic: "Calculate the sum of 25 and 15"
|
||||
|
||||
agents:
|
||||
calculator_agent:
|
||||
role: Calculator
|
||||
goal: Perform calculations
|
||||
instructions: "Use the add_numbers tool to help with calculations"
|
||||
tools:
|
||||
- add_numbers
|
||||
```
|
||||
|
||||
**tools.py:**
|
||||
```python
|
||||
def add_numbers(a: float, b: float) -> float:
|
||||
"""
|
||||
Add two numbers together.
|
||||
|
||||
Args:
|
||||
a: First number
|
||||
b: Second number
|
||||
|
||||
Returns:
|
||||
The sum of a and b
|
||||
"""
|
||||
return a + b
|
||||
```
|
||||
|
||||
Run with:
|
||||
```bash
|
||||
praisonai agents.yaml
|
||||
```
|
||||
|
||||
> 💡 **Tips:**
|
||||
> - Use the function name (e.g., `add_numbers`) in the tools list, not the file name
|
||||
> - Tools in `tools.py` are automatically discovered
|
||||
> - The function's docstring helps the AI understand how to use it
|
||||
|
||||
---
|
||||
|
||||
## 🎯 CLI Quick Reference
|
||||
|
||||
@@ -28,39 +28,23 @@ from praisonaiagents import Agent, AutoAgents, AutoRagAgent, ContextAgent, DeepR
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">achat</a>(prompt: str, temperature = 1.0, tools = None, output_json = None, output_pydantic = None, reasoning_steps = False, task_name = None, task_description = None, task_id = None, attachments = None)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">aexecute</a>(task, context = None)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">agent_id</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">agent_id</a>() -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">analyze_prompt</a>(prompt: str) -> set</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">arun</a>(prompt: str, **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">as_tool</a>(description: Optional[str] = None, tool_name: Optional[str] = None) -> 'Handoff'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">astart</a>(prompt: str, **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat</a>(prompt, temperature = 1.0, tools = None, output_json = None, output_pydantic = None, reasoning_steps = False, stream = None, task_name = None, task_description = None, task_id = None, config = None, force_retrieval = False, skip_retrieval = False, attachments = None, tool_choice = None)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>() -> Optional[bool]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>(value: Optional[bool]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>() -> Optional[bool]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>(value: Optional[bool]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_with_context</a>(message: str, context: 'ContextPack', **kwargs) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">check_pending_approvals</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">clean_json_output</a>(output: str) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">clear_history</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">console</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">context_manager</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>() -> Optional[bool]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>(value: Optional[bool]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">console</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">context_manager</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">context_manager</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">delete_history</a>(index: int) -> bool</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">delete_history_matching</a>(pattern: str) -> int</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">cost_summary</a>() -> dict</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">diff</a>(from_hash: Optional[str] = None)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">display_name</a>() -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">ephemeral</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">execute</a>(task, context = None)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">execute_tool</a>(function_name, arguments, tool_call_id = None)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">execute_tool_async</a>(function_name: str, arguments: Dict[str, Any]) -> Any</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">from_template</a>(uri: str, config: Optional[Dict[str, Any]] = None, offline: bool = False, **kwargs) -> 'Agent'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">generate_task</a>() -> 'Task'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_available_tools</a>() -> List[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_history_size</a>() -> int</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_learn_context</a>() -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_memory_context</a>(query: Optional[str] = None) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_recommended_stage</a>(prompt: str) -> str</code>
|
||||
@@ -68,37 +52,29 @@ Methods:
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_skills_prompt</a>() -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to_async</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">iter_stream</a>(prompt: str, **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">launch</a>(path: str = '/', port: int = 8000, host: str = '0.0.0.0', debug: bool = False, protocol: str = 'http')</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">llm_model</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">output_style</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">output_style</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">pending_approval_count</a>() -> int</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">policy</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">policy</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">prune_history</a>(keep_last: int = 5) -> int</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">llm_model</a>() -> Optional[str]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">output_style</a>() -> Optional[str]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">output_style</a>(value: Optional[str]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">policy</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">policy</a>(value: Optional[Any]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">query</a>(question: str, **kwargs) -> 'RAGResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rag</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rag</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rag_query</a>(question: str, **kwargs) -> 'RAGResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">redo</a>() -> bool</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">retrieval_config</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">retrieval_config</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">retrieve</a>(query: str, **kwargs) -> 'ContextPack'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rules_manager</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run</a>(prompt: str, **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rules_manager</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None, completion_promise: Optional[str] = None, clear_context: bool = False)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous_async</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None, completion_promise: Optional[str] = None, clear_context: bool = False)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until</a>(prompt: str, criteria: str, threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False) -> 'EvaluationLoopResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until_async</a>(prompt: str, criteria: str, threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False) -> 'EvaluationLoopResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">session_id</a>() -> Optional[str]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">skill_manager</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">start</a>(prompt: str = None, **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">store_memory</a>(content: str, memory_type: str = 'short_term', **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">submit_for_approval</a>(function_name: str, arguments: Dict[str, Any]) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">switch_model</a>(new_model: str) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">skill_manager</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">store_memory</a>(content: str, memory_type: str = 'short_term', **kwargs: Any) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>() -> Optional[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>(value: Optional[Any]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>() -> Optional[int]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>(value: Optional[int]) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">total_cost</a>() -> float</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">undo</a>() -> bool</code>
|
||||
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">astart</a>()</code>
|
||||
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">start</a>()</code>
|
||||
@@ -250,6 +226,7 @@ Methods:
|
||||
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">build_context_for_task</a>(task_descr: str, user_id: Optional[str] = None, additional: str = '', max_items: int = 3, include_in_output: Optional[bool] = None) -> str</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">calculate_quality_metrics</a>(output: str, expected_output: str, llm: Optional[str] = None, custom_prompt: Optional[str] = None) -> Dict[str, float]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">close_connections</a>()</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">compute_quality_score</a>(completeness: float, relevance: float, clarity: float, accuracy: float, weights: Dict[str, float] = None) -> float</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_long_term</a>(memory_id: str) -> bool</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_memories</a>(memory_ids: List[str]) -> int</code>
|
||||
@@ -750,8 +727,6 @@ Methods:
|
||||
* <code title="class AutoApproveBackend">AutoApproveBackend.<a href="./src/praisonai-agents/praisonaiagents/approval/backends.py">request_approval_sync</a>(request: ApprovalRequest) -> ApprovalDecision</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">aembedding</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">aembeddings</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">embedding</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">embeddings</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/dimensions.py">get_dimensions</a>(model_name: str) -> int</code>
|
||||
|
||||
# Wrapper (praisonai)
|
||||
@@ -812,6 +787,8 @@ Methods:
|
||||
* <code title="cli">praisonai completion completion-callback <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion fish <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion zsh <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai config doctor <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config env <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config get <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config list <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config path <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
@@ -871,6 +848,8 @@ Methods:
|
||||
* <code title="cli">praisonai examples run <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples run-all <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples stats <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai flow flow-start <a href="./src/praisonai/praisonai/cli/commands/flow.py">--help</a></code>
|
||||
* <code title="cli">praisonai flow version <a href="./src/praisonai/praisonai/cli/commands/flow.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway channels <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway gateway-callback <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway send <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
@@ -1042,12 +1021,7 @@ Methods:
|
||||
* <code title="cli">praisonai train llm <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
|
||||
* <code title="cli">praisonai train show <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
|
||||
* <code title="cli">praisonai train train-callback <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui bot <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui chat <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui code <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui gradio <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui realtime <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui ui-main <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui ui <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai version check <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
|
||||
* <code title="cli">praisonai version show <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
|
||||
* <code title="cli">praisonai version version-callback <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
|
||||
@@ -1200,7 +1174,7 @@ export { // Python parity additions
|
||||
export { // Subagent Tool (agent-as-tool pattern)
|
||||
SubagentTool, BaseTool, FunctionTool, ToolRegistry, ToolResult, ToolValidationError, createDelegator, createSubagentTool, createSubagentTools, createTool, getRegistry, getTool, registerTool, tool, validateTool } from "./tools";
|
||||
export { airweaveSearch, bedrockBrowserClick, bedrockBrowserFill, bedrockBrowserNavigate, bedrockCodeInterpreter, codeExecution, codeMode, createCustomTool, exaSearch, firecrawlCrawl, firecrawlScrape, parallelSearch, perplexitySearch, registerCustomTool, registerLocalTool, registerNpmTool, superagentGuard, superagentRedact, superagentVerify, tavilyCrawl, tavilyExtract, tavilySearch, valyuBioSearch, valyuCompanyResearch, valyuEconomicsSearch, valyuFinanceSearch, valyuPaperSearch, valyuPatentSearch, valyuSecSearch, valyuWebSearch } from "./tools/builtins";
|
||||
export { MissingDependencyError, MissingEnvVarError, ToolsRegistry, composeMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRedactionMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolsRegistry, createTracingMiddleware, createValidationMiddleware, getToolsRegistry, resetToolsRegistry } from "./tools/registry";
|
||||
export { BudgetExceededError, MissingDependencyError, MissingEnvVarError, ToolsRegistry, composeMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRedactionMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolsRegistry, createTracingMiddleware, createValidationMiddleware, getRegistry, getTool, getToolsRegistry, get_registry, get_tool, registerTool, register_tool, resetToolsRegistry, validateTool, validate_tool } from "./tools/registry";
|
||||
export type { InstallHints, PraisonTool, RedactionHooks, RegisteredTool, ToolCapabilities, ToolExecutionContext, ToolExecutionResult, ToolFactory, ToolHooks, ToolInstallStatus, ToolLimits, ToolLogger, ToolMetadata, ToolMiddleware, ToolParameterProperty, ToolParameterSchema } from "./tools/registry";
|
||||
export { registerBuiltinTools, tools } from "./tools/tools";
|
||||
export { // Classes
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.5.71" \
|
||||
"praisonai>=4.5.112" \
|
||||
"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>=4.5.71" \
|
||||
"praisonai>=4.5.112" \
|
||||
"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>=4.5.71" \
|
||||
"praisonai>=4.5.112" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
framework: ag2
|
||||
topic: "Research the latest developments in AI agents"
|
||||
|
||||
# Install: pip install "praisonai[ag2]"
|
||||
# Run: praisonai --framework ag2 examples/ag2/ag2_basic.yaml
|
||||
# or praisonai run examples/ag2/ag2_basic.yaml --framework ag2
|
||||
|
||||
roles:
|
||||
research_agent:
|
||||
role: "AI Research Specialist"
|
||||
goal: "Research and summarise the latest developments in AI agent frameworks"
|
||||
backstory: |
|
||||
You are an experienced AI researcher with deep knowledge of multi-agent
|
||||
systems, large language models, and the latest trends in AI tooling.
|
||||
You excel at synthesising complex technical topics into clear summaries.
|
||||
tasks:
|
||||
research_task:
|
||||
description: |
|
||||
Research and summarise the latest developments in AI agent frameworks
|
||||
for the topic: {topic}
|
||||
|
||||
Focus on:
|
||||
1. Key frameworks and their unique capabilities
|
||||
2. Recent innovations and improvements
|
||||
3. Community adoption and ecosystem growth
|
||||
4. Practical use cases and success stories
|
||||
expected_output: |
|
||||
A concise research summary covering the key developments,
|
||||
major frameworks, and practical insights. Include 3-5 bullet
|
||||
points of the most important findings.
|
||||
@@ -0,0 +1,42 @@
|
||||
framework: ag2
|
||||
topic: "Cloud-native AI deployment strategies on AWS"
|
||||
|
||||
# AG2 exclusive feature: native AWS Bedrock support via LLMConfig(api_type="bedrock")
|
||||
#
|
||||
# Prerequisites:
|
||||
# pip install "praisonai[ag2]"
|
||||
# aws configure (or set AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION)
|
||||
#
|
||||
# Run:
|
||||
# praisonai --framework ag2 examples/ag2/ag2_bedrock.yaml
|
||||
#
|
||||
# The AG2 adapter detects api_type="bedrock" from the llm config and uses
|
||||
# LLMConfig(api_type="bedrock", model=...) — no OPENAI_API_KEY required.
|
||||
# AWS credentials are sourced from boto3 (env vars, ~/.aws/credentials, IAM role).
|
||||
|
||||
roles:
|
||||
cloud_architect:
|
||||
role: "AWS Cloud Architect"
|
||||
goal: "Design and explain cloud-native AI deployment strategies on AWS"
|
||||
backstory: |
|
||||
You are an AWS Solutions Architect specialising in AI/ML workloads.
|
||||
You have deep expertise in Amazon Bedrock, SageMaker, ECS, and Lambda,
|
||||
and you help organisations deploy AI agents at scale securely and cost-effectively.
|
||||
llm:
|
||||
model: "bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0"
|
||||
api_type: "bedrock"
|
||||
aws_region: "us-east-1"
|
||||
tasks:
|
||||
architecture_task:
|
||||
description: |
|
||||
Design a cloud-native deployment strategy for AI agents on AWS for: {topic}
|
||||
|
||||
Cover:
|
||||
1. Recommended AWS services (Bedrock, ECS, Lambda, etc.)
|
||||
2. Scalability and cost optimisation patterns
|
||||
3. Security and compliance considerations
|
||||
4. A simple reference architecture overview
|
||||
expected_output: |
|
||||
A concise architecture guide with service recommendations,
|
||||
a high-level deployment diagram description, and key
|
||||
best practices for production AI agent deployments on AWS.
|
||||
@@ -0,0 +1,54 @@
|
||||
framework: ag2
|
||||
topic: "The impact of open-source AI on enterprise software development"
|
||||
|
||||
# Install: pip install "praisonai[ag2]"
|
||||
# Run: praisonai --framework ag2 examples/ag2/ag2_multi_agent.yaml
|
||||
# or praisonai run examples/ag2/ag2_multi_agent.yaml --framework ag2
|
||||
#
|
||||
# This example demonstrates AG2's GroupChat multi-agent coordination.
|
||||
# Both agents participate in a collaborative conversation managed by
|
||||
# a GroupChatManager until the task is complete.
|
||||
|
||||
roles:
|
||||
researcher:
|
||||
role: "Research Specialist"
|
||||
goal: "Gather and analyse information on the given topic"
|
||||
backstory: |
|
||||
You are a meticulous researcher who excels at finding relevant
|
||||
information, analysing trends, and presenting data-backed insights.
|
||||
You always cite your reasoning and structure your findings clearly.
|
||||
tasks:
|
||||
research_task:
|
||||
description: |
|
||||
Research the topic: {topic}
|
||||
|
||||
Investigate:
|
||||
1. Current state and adoption rates
|
||||
2. Key players and projects driving the trend
|
||||
3. Technical advantages and challenges
|
||||
4. Business impact and cost implications
|
||||
expected_output: |
|
||||
A structured research briefing with findings on the topic,
|
||||
including key data points, trends, and technical observations.
|
||||
|
||||
writer:
|
||||
role: "Technical Content Writer"
|
||||
goal: "Transform research findings into clear, engaging written content"
|
||||
backstory: |
|
||||
You are a skilled technical writer who turns complex research into
|
||||
accessible, well-structured articles. You focus on clarity, logical
|
||||
flow, and actionable takeaways for a professional audience.
|
||||
tasks:
|
||||
writing_task:
|
||||
description: |
|
||||
Using the research findings provided by the Research Specialist,
|
||||
write a concise article on: {topic}
|
||||
|
||||
The article should:
|
||||
1. Open with a compelling hook
|
||||
2. Present key findings logically
|
||||
3. Include practical implications for developers
|
||||
4. Close with a forward-looking conclusion
|
||||
expected_output: |
|
||||
A 400-500 word article suitable for a technical blog,
|
||||
with clear sections, professional tone, and concrete takeaways.
|
||||
@@ -37,7 +37,7 @@ def example_simple_enable():
|
||||
instructions="You are a helpful assistant",
|
||||
memory=True, # Enable memory with defaults
|
||||
reflection=True, # Enable self-reflection
|
||||
web=True, # Enable web search + fetch
|
||||
web=WebConfig(fetch=False), # Enable web search + fetch
|
||||
)
|
||||
print(f"Agent created with memory={agent._memory_instance is not None}")
|
||||
print(f" self_reflect={agent.self_reflect}")
|
||||
@@ -152,7 +152,7 @@ def example_string_presets():
|
||||
instructions="You are a verbose assistant",
|
||||
output="verbose", # String preset for output
|
||||
execution="fast", # String preset for execution
|
||||
web="tavily", # String preset for web provider
|
||||
web=["tavily", {"fetch": False}], # String preset with override for web provider
|
||||
)
|
||||
print(f"Output preset 'verbose': verbose={agent.verbose}, metrics={agent.metrics}")
|
||||
print(f"Execution preset 'fast': max_iter={agent.max_iter}")
|
||||
|
||||
@@ -5,14 +5,14 @@ both files to verify everything looks good.
|
||||
"""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools import (
|
||||
write_file, read_file, execute_command, list_directory
|
||||
write_file, read_file, execute_command, list_files
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful coding assistant.",
|
||||
output="editor",
|
||||
tools=[write_file, read_file, execute_command, list_directory],
|
||||
approval=True,
|
||||
tools=[write_file, read_file, execute_command, list_files],
|
||||
approval=False,
|
||||
)
|
||||
agent.start(
|
||||
"Create a Python file at /tmp/fibonacci.py that generates the first 20 "
|
||||
|
||||
@@ -5,22 +5,22 @@ markdown report, reads it back, lists files, and gets system info.
|
||||
"""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools import (
|
||||
write_file, read_file, execute_command, list_directory, get_system_info
|
||||
write_file, read_file, execute_command, list_files, get_system_info
|
||||
)
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools import web_search
|
||||
tools = [web_search, write_file, read_file, execute_command,
|
||||
list_directory, get_system_info]
|
||||
list_files, get_system_info]
|
||||
except ImportError:
|
||||
tools = [write_file, read_file, execute_command,
|
||||
list_directory, get_system_info]
|
||||
list_files, get_system_info]
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful research assistant.",
|
||||
output="editor",
|
||||
tools=tools,
|
||||
approval=True,
|
||||
approval=False,
|
||||
)
|
||||
agent.start(
|
||||
"Research the top 3 Python web frameworks (Django, FastAPI, Flask), then: "
|
||||
|
||||
@@ -5,20 +5,20 @@ it, and lists created files.
|
||||
"""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.tools import (
|
||||
write_file, execute_command, list_directory
|
||||
write_file, execute_command, list_files
|
||||
)
|
||||
|
||||
try:
|
||||
from praisonaiagents.tools import web_search
|
||||
tools = [web_search, write_file, execute_command, list_directory]
|
||||
tools = [web_search, write_file, execute_command, list_files]
|
||||
except ImportError:
|
||||
tools = [write_file, execute_command, list_directory]
|
||||
tools = [write_file, execute_command, list_files]
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful research assistant.",
|
||||
output="editor",
|
||||
tools=tools,
|
||||
approval=True,
|
||||
approval=False,
|
||||
)
|
||||
agent.start(
|
||||
"Search the web for the current weather in Tokyo, create a JSON file at "
|
||||
|
||||
@@ -19,7 +19,15 @@ async function main() {
|
||||
},
|
||||
execute: async ({ expression }) => {
|
||||
try {
|
||||
const result = eval(expression);
|
||||
// Only allow safe numeric math characters
|
||||
if (!/^[0-9+\-*/.() ]+$/.test(expression)) {
|
||||
return 'Error: Only basic math expressions are allowed';
|
||||
}
|
||||
// Use Function constructor scoped to math only, no globals
|
||||
const result = new Function(`"use strict"; return (${expression})`)();
|
||||
if (typeof result !== 'number' || !isFinite(result)) {
|
||||
return 'Error: Invalid result';
|
||||
}
|
||||
return `Result: ${result}`;
|
||||
} catch (e) {
|
||||
return 'Error: Invalid expression';
|
||||
|
||||
@@ -38,7 +38,13 @@ const calculatorTool = createTool({
|
||||
},
|
||||
run: (params: { expression: string }) => {
|
||||
try {
|
||||
return eval(params.expression);
|
||||
// Only allow safe numeric math characters
|
||||
if (!/^[0-9+\-*/.() ]+$/.test(params.expression)) {
|
||||
return 'Error: Only basic math expressions are allowed';
|
||||
}
|
||||
const result = new Function(`"use strict"; return (${params.expression})`)();
|
||||
if (typeof result !== 'number' || !isFinite(result)) return 'Error: Invalid result';
|
||||
return result;
|
||||
} catch {
|
||||
return 'Error: Invalid expression';
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ from praisonai.persistence import create_conversation_store
|
||||
|
||||
store = create_conversation_store("sqlite", path="/tmp/multi_agent.db")
|
||||
|
||||
agent1 = Agent(name="Researcher", llm="gpt-4o-mini", db=store, session_id="shared-session")
|
||||
agent2 = Agent(name="Writer", llm="gpt-4o-mini", db=store, session_id="shared-session")
|
||||
agent1 = Agent(name="Researcher", llm="gpt-4o-mini", memory={"backend": "sqlite", "session_id": "shared-session", "db": store})
|
||||
agent2 = Agent(name="Writer", llm="gpt-4o-mini", memory={"backend": "sqlite", "session_id": "shared-session", "db": store})
|
||||
|
||||
r1 = agent1.chat("Research topic: AI in healthcare. Key finding: reduces diagnosis time by 50%")
|
||||
print(f"Researcher: {r1[:50]}...")
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
SandlockSandbox Security Demo
|
||||
|
||||
Demonstrates how to use SandlockSandbox for kernel-level code isolation.
|
||||
Requires: pip install 'praisonai[sandbox]' (installs sandlock)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from praisonaiagents.sandbox import ResourceLimits, SecurityPolicy, SandboxConfig
|
||||
|
||||
|
||||
async def demonstrate_sandlock_security():
|
||||
"""Show SandlockSandbox blocking malicious operations."""
|
||||
|
||||
try:
|
||||
from praisonai.sandbox import SandlockSandbox
|
||||
except ImportError:
|
||||
print("❌ SandlockSandbox requires: pip install 'praisonai[sandbox]'")
|
||||
print(" Falling back to SubprocessSandbox (less secure)")
|
||||
from praisonai.sandbox import SubprocessSandbox
|
||||
sandbox = SubprocessSandbox()
|
||||
else:
|
||||
# Create sandbox with strict security policy
|
||||
config = SandboxConfig.native(
|
||||
writable_paths=["./safe_workspace"], # Only allow writes to this dir
|
||||
network=False, # Block all network access
|
||||
)
|
||||
sandbox = SandlockSandbox(config)
|
||||
print("✅ Using SandlockSandbox with kernel-level isolation")
|
||||
|
||||
await sandbox.start()
|
||||
|
||||
# Test 1: Safe code execution
|
||||
print("\n🟢 Test 1: Safe Python code")
|
||||
result = await sandbox.execute(
|
||||
"print('Hello from isolated sandbox!')",
|
||||
limits=ResourceLimits.minimal()
|
||||
)
|
||||
print(f"Status: {result.status}")
|
||||
print(f"Output: {result.stdout}")
|
||||
|
||||
# Test 2: Attempt file system access (should be blocked by Landlock)
|
||||
print("\n🔒 Test 2: Attempt to read sensitive files")
|
||||
result = await sandbox.execute("""
|
||||
try:
|
||||
with open('/etc/passwd', 'r') as f:
|
||||
print(f.read()[:100])
|
||||
except Exception as e:
|
||||
print(f'Access blocked: {e}')
|
||||
""")
|
||||
print(f"Status: {result.status}")
|
||||
print(f"Output: {result.stdout}")
|
||||
if result.stderr:
|
||||
print(f"Stderr: {result.stderr}")
|
||||
|
||||
# Test 3: Attempt network access (should be blocked)
|
||||
print("\n🌐 Test 3: Attempt network access")
|
||||
result = await sandbox.execute("""
|
||||
try:
|
||||
import socket
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.connect(('example.com', 80))
|
||||
print('Network connection successful')
|
||||
s.close()
|
||||
except Exception as e:
|
||||
print(f'Network blocked: {e}')
|
||||
""")
|
||||
print(f"Status: {result.status}")
|
||||
print(f"Output: {result.stdout}")
|
||||
|
||||
# Test 4: Attempt subprocess execution (blocked by seccomp)
|
||||
print("\n🚫 Test 4: Attempt subprocess execution")
|
||||
result = await sandbox.execute("""
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(['ls', '/'], capture_output=True, text=True)
|
||||
print(f'Command output: {result.stdout}')
|
||||
except Exception as e:
|
||||
print(f'Subprocess blocked: {e}')
|
||||
""")
|
||||
print(f"Status: {result.status}")
|
||||
print(f"Output: {result.stdout}")
|
||||
|
||||
# Test 5: Resource limits in action
|
||||
print("\n⏱️ Test 5: Resource limits (timeout)")
|
||||
result = await sandbox.execute(
|
||||
"import time; time.sleep(100)", # Will timeout
|
||||
limits=ResourceLimits(timeout_seconds=2)
|
||||
)
|
||||
print(f"Status: {result.status}")
|
||||
print(f"Error: {result.error}")
|
||||
|
||||
await sandbox.stop()
|
||||
|
||||
# Show security features
|
||||
status = sandbox.get_status()
|
||||
if 'features' in status:
|
||||
print(f"\n🛡️ Security features enabled:")
|
||||
for feature, enabled in status['features'].items():
|
||||
emoji = "✅" if enabled else "❌"
|
||||
print(f" {emoji} {feature}: {enabled}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🔒 SandlockSandbox Security Demonstration")
|
||||
print("="*50)
|
||||
|
||||
asyncio.run(demonstrate_sandlock_security())
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("🎯 Key Security Benefits:")
|
||||
print(" • Landlock: Kernel-enforced filesystem allowlisting")
|
||||
print(" • seccomp-bpf: System call filtering")
|
||||
print(" • Resource limits: Memory, CPU, process constraints")
|
||||
print(" • Network isolation: Block unauthorized connections")
|
||||
print(" • ~5ms overhead: Faster than Docker containers")
|
||||
print(" • No root required: User-space security")
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Social Media Cross-Posting Agent Example.
|
||||
|
||||
Demonstrates using PraisonAI Agent with all social media tools
|
||||
to cross-post content across multiple platforms.
|
||||
|
||||
Required environment variables (set only the ones you need):
|
||||
# X (Twitter)
|
||||
X_API_KEY, X_API_SECRET, X_ACCESS_TOKEN, X_ACCESS_SECRET
|
||||
|
||||
# LinkedIn
|
||||
LINKEDIN_ACCESS_TOKEN, LINKEDIN_PERSON_URN
|
||||
|
||||
# Facebook
|
||||
FACEBOOK_PAGE_ACCESS_TOKEN, FACEBOOK_PAGE_ID
|
||||
|
||||
# Instagram
|
||||
INSTAGRAM_ACCESS_TOKEN, INSTAGRAM_BUSINESS_ACCOUNT_ID
|
||||
|
||||
# TikTok
|
||||
TIKTOK_ACCESS_TOKEN
|
||||
|
||||
# Threads
|
||||
THREADS_ACCESS_TOKEN, THREADS_USER_ID
|
||||
|
||||
# Pinterest
|
||||
PINTEREST_ACCESS_TOKEN
|
||||
|
||||
# Medium
|
||||
MEDIUM_TOKEN
|
||||
|
||||
# WordPress (uses praisonaiwp-cli config — no env vars needed)
|
||||
|
||||
# OpenAI (for the agent)
|
||||
OPENAI_API_KEY
|
||||
|
||||
Usage:
|
||||
python social_media_crosspost.py
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
from praisonai_tools import (
|
||||
# X (Twitter)
|
||||
post_to_x,
|
||||
# LinkedIn
|
||||
linkedin_post_text,
|
||||
linkedin_post_image,
|
||||
linkedin_post_article,
|
||||
# Facebook
|
||||
facebook_post_text,
|
||||
facebook_post_image,
|
||||
facebook_post_link,
|
||||
# Instagram
|
||||
instagram_post_image,
|
||||
instagram_post_reel,
|
||||
# TikTok
|
||||
tiktok_post_video,
|
||||
tiktok_post_photo,
|
||||
# Threads
|
||||
threads_post_text,
|
||||
threads_post_image,
|
||||
# Pinterest
|
||||
pinterest_create_pin,
|
||||
pinterest_list_boards,
|
||||
# Medium
|
||||
medium_publish_post,
|
||||
# WordPress
|
||||
create_wp_post,
|
||||
check_wp_duplicate,
|
||||
)
|
||||
|
||||
# Create a social media manager agent
|
||||
agent = Agent(
|
||||
name="Social Media Manager",
|
||||
instructions="""You are a social media manager. You help users cross-post content
|
||||
across multiple platforms including X (Twitter), LinkedIn, Facebook, Instagram,
|
||||
TikTok, Threads, Pinterest, Medium, and WordPress.
|
||||
|
||||
When asked to cross-post:
|
||||
1. Adapt the content format for each platform (character limits, hashtags, etc.)
|
||||
2. Use the appropriate tool for each platform
|
||||
3. Report which platforms succeeded and which had errors
|
||||
|
||||
Platform-specific notes:
|
||||
- X: 280 char limit
|
||||
- LinkedIn: 3000 char limit, professional tone
|
||||
- Facebook: No strict limit, supports links
|
||||
- Instagram: Needs image URL (public URL), max 2200 chars caption
|
||||
- TikTok: Video/photo only, 150 char title
|
||||
- Threads: 500 char limit
|
||||
- Pinterest: Needs board_id and image URL
|
||||
- Medium: Full article support (markdown/HTML), default to 'draft' status
|
||||
- WordPress: Full article support (HTML/Gutenberg blocks), check duplicates first
|
||||
""",
|
||||
tools=[
|
||||
post_to_x,
|
||||
linkedin_post_text,
|
||||
linkedin_post_image,
|
||||
linkedin_post_article,
|
||||
facebook_post_text,
|
||||
facebook_post_image,
|
||||
facebook_post_link,
|
||||
instagram_post_image,
|
||||
instagram_post_reel,
|
||||
tiktok_post_video,
|
||||
tiktok_post_photo,
|
||||
threads_post_text,
|
||||
threads_post_image,
|
||||
pinterest_create_pin,
|
||||
pinterest_list_boards,
|
||||
medium_publish_post,
|
||||
create_wp_post,
|
||||
check_wp_duplicate,
|
||||
],
|
||||
)
|
||||
|
||||
# Example: Post to all text-based platforms
|
||||
result = agent.start(
|
||||
"Post this announcement to LinkedIn and Threads: "
|
||||
"'PraisonAI v2.0 is here! Build AI agents in 3 lines of code. "
|
||||
"Multi-agent workflows, memory, tools, and more. Try it: pip install praisonaiagents'"
|
||||
)
|
||||
print(result)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""HeyGen + ElevenLabs Video Generation Example.
|
||||
|
||||
Generate an AI avatar video using HeyGen with ElevenLabs voice.
|
||||
The agent autonomously: generates audio → uploads to HeyGen → creates video → waits for completion.
|
||||
|
||||
Required env vars:
|
||||
HEYGEN_API_KEY: HeyGen API key (https://app.heygen.com/settings/api)
|
||||
ELEVENLABS_API_KEY: ElevenLabs API key (https://elevenlabs.io)
|
||||
OPENAI_API_KEY: OpenAI API key (for the agent LLM)
|
||||
|
||||
Usage:
|
||||
export HEYGEN_API_KEY=your_key
|
||||
export ELEVENLABS_API_KEY=your_key
|
||||
export OPENAI_API_KEY=your_key
|
||||
python video_heygen_elevenlabs.py
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonai_tools import (
|
||||
elevenlabs_speak_to_file,
|
||||
heygen_upload_asset,
|
||||
heygen_generate_video_with_audio,
|
||||
heygen_wait_for_video,
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="Video Producer",
|
||||
instructions="""You create AI avatar videos. Follow these steps:
|
||||
|
||||
Step 1: Call elevenlabs_speak_to_file with text and output_path="/tmp/speech.mp3"
|
||||
Step 2: Call heygen_upload_asset with file_path="/tmp/speech.mp3", content_type="audio/mpeg"
|
||||
Step 3: Call heygen_generate_video_with_audio with the avatar_id and audio_asset_id from Step 2
|
||||
Step 4: Call heygen_wait_for_video with the video_id from Step 3
|
||||
Step 5: Report the final video URL
|
||||
""",
|
||||
tools=[
|
||||
elevenlabs_speak_to_file,
|
||||
heygen_upload_asset,
|
||||
heygen_generate_video_with_audio,
|
||||
heygen_wait_for_video,
|
||||
],
|
||||
)
|
||||
|
||||
result = agent.start(
|
||||
'Create a video with this script: "Welcome to PraisonAI! '
|
||||
'Build powerful AI agents in just three lines of code." '
|
||||
'Use avatar_id: "YOUR_AVATAR_ID"'
|
||||
)
|
||||
print(result)
|
||||
@@ -134,10 +134,28 @@ def main():
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
# Safe AST-based math evaluator (no eval on arbitrary expressions)
|
||||
def _safe_calculate(expression: str) -> str:
|
||||
import ast
|
||||
try:
|
||||
allowed = set("0123456789+-*/.() ")
|
||||
if not all(c in allowed for c in expression):
|
||||
return "Error: Invalid characters in expression"
|
||||
tree = ast.parse(expression, mode="eval")
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, (ast.Expression, ast.BinOp, ast.UnaryOp,
|
||||
ast.Constant, ast.Add, ast.Sub, ast.Mult,
|
||||
ast.Div, ast.USub, ast.UAdd)):
|
||||
return "Error: Only basic math operations are allowed"
|
||||
result = eval(compile(tree, "<expr>", "eval"), {"__builtins__": {}})
|
||||
return f"Result: {result}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
# MCP endpoints
|
||||
mcp_tools = {
|
||||
"search": {"description": "Search the web", "func": lambda q: f"Results for: {q}"},
|
||||
"calculate": {"description": "Calculate expression", "func": lambda e: str(eval(e)) if e.replace(" ", "").replace("+", "").replace("-", "").replace("*", "").replace("/", "").replace(".", "").isdigit() or True else "Error"},
|
||||
"calculate": {"description": "Calculate expression", "func": _safe_calculate},
|
||||
}
|
||||
|
||||
@app.get("/mcp/tools")
|
||||
|
||||
@@ -5,11 +5,7 @@ from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant with access to documents.",
|
||||
knowledge=["./docs/guide.pdf"], # Add your documents here
|
||||
knowledge={
|
||||
"vector_store": "chroma",
|
||||
"path": "/tmp/chroma_demo"
|
||||
}
|
||||
knowledge={"sources": ["./docs/guide.pdf"], "vector_store": {"provider": "chroma", "path": "/tmp/chroma_demo"}}
|
||||
)
|
||||
|
||||
# Chat - agent uses knowledge for RAG
|
||||
|
||||
@@ -5,11 +5,7 @@ from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant with access to documents.",
|
||||
knowledge=["./docs/guide.pdf"], # Add your documents here
|
||||
knowledge={
|
||||
"vector_store": "lancedb",
|
||||
"path": "/tmp/lancedb_test"
|
||||
}
|
||||
knowledge={"sources": ["./docs/guide.pdf"], "vector_store": {"provider": "lancedb", "path": "/tmp/lancedb_test"}}
|
||||
)
|
||||
|
||||
# Chat - agent uses knowledge for RAG
|
||||
|
||||
@@ -5,11 +5,7 @@ from praisonaiagents import Agent
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant with access to documents.",
|
||||
knowledge=["./docs/guide.pdf"], # Add your documents here
|
||||
knowledge={
|
||||
"vector_store": "lancedb",
|
||||
"path": "/tmp/lancedb_demo"
|
||||
}
|
||||
knowledge={"sources": ["./docs/guide.pdf"], "vector_store": {"provider": "lancedb", "path": "/tmp/lancedb_demo"}}
|
||||
)
|
||||
|
||||
# Chat - agent uses knowledge for RAG
|
||||
|
||||
@@ -11,11 +11,7 @@ url = os.getenv("PGVECTOR_URL", "postgresql://postgres:postgres@localhost:5433/p
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant with access to documents.",
|
||||
knowledge=["./docs/guide.pdf"], # Add your documents here
|
||||
knowledge={
|
||||
"vector_store": "pgvector",
|
||||
"url": url
|
||||
}
|
||||
knowledge={"sources": ["./docs/guide.pdf"], "vector_store": {"provider": "pgvector", "url": url}}
|
||||
)
|
||||
|
||||
# Chat - agent uses knowledge for RAG
|
||||
|
||||
@@ -11,11 +11,7 @@ url = os.getenv("PGVECTOR_URL", "postgresql://postgres:postgres@localhost:5433/p
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant with access to documents.",
|
||||
knowledge=["./docs/guide.pdf"], # Add your documents here
|
||||
knowledge={
|
||||
"vector_store": "pgvector",
|
||||
"url": url
|
||||
}
|
||||
knowledge={"sources": ["./docs/guide.pdf"], "vector_store": {"provider": "pgvector", "url": url}}
|
||||
)
|
||||
|
||||
# Chat - agent uses knowledge for RAG
|
||||
|
||||
@@ -15,8 +15,8 @@ if not api_key:
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant with access to documents.",
|
||||
knowledge=["./docs/guide.pdf"], # Add your documents here
|
||||
knowledge={
|
||||
"sources": ["./docs/guide.pdf"],
|
||||
"vector_store": "pinecone",
|
||||
"api_key": api_key,
|
||||
"environment": "us-east-1"
|
||||
|
||||
@@ -11,11 +11,7 @@ url = os.getenv("QDRANT_URL", "http://localhost:6333")
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant with access to documents.",
|
||||
knowledge=["./docs/guide.pdf"], # Add your documents here
|
||||
knowledge={
|
||||
"vector_store": "qdrant",
|
||||
"url": url
|
||||
}
|
||||
knowledge={"sources": ["./docs/guide.pdf"], "vector_store": {"provider": "qdrant", "url": url}}
|
||||
)
|
||||
|
||||
# Chat - agent uses knowledge for RAG
|
||||
|
||||
@@ -16,8 +16,8 @@ if not weaviate_key:
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant with access to documents.",
|
||||
knowledge=["./docs/guide.pdf"], # Add your documents here
|
||||
knowledge={
|
||||
"sources": ["./docs/guide.pdf"],
|
||||
"vector_store": "weaviate",
|
||||
"url": weaviate_url,
|
||||
"api_key": weaviate_key
|
||||
|
||||
@@ -34,14 +34,14 @@ variables:
|
||||
agents: # Canonical: use 'agents' instead of 'roles'
|
||||
classifier:
|
||||
role: Request Classifier
|
||||
instructions: # Canonical |
|
||||
instructions: | # Canonical
|
||||
You are an expert at classifying requests into categories.
|
||||
Respond with ONLY one word: 'technical', 'creative', or 'research'.
|
||||
goal: Classify incoming requests accurately
|
||||
|
||||
researcher:
|
||||
role: Research Analyst
|
||||
instructions: # Canonical |
|
||||
instructions: | # Canonical
|
||||
You are a skilled research analyst with expertise in technology trends.
|
||||
You provide concise, factual research findings with citations.
|
||||
goal: Research topics thoroughly and provide insights
|
||||
@@ -52,28 +52,28 @@ agents: # Canonical: use 'agents' instead of 'roles'
|
||||
|
||||
tech_expert:
|
||||
role: Technical Expert
|
||||
instructions: # Canonical |
|
||||
instructions: | # Canonical
|
||||
You are a technical expert with deep knowledge in AI and ML.
|
||||
You provide detailed technical explanations.
|
||||
goal: Handle technical questions with expertise
|
||||
|
||||
creative_writer:
|
||||
role: Creative Writer
|
||||
instructions: # Canonical |
|
||||
instructions: | # Canonical
|
||||
You are a creative writer who can make complex topics engaging.
|
||||
You write in a clear, accessible style.
|
||||
goal: Create engaging content from research
|
||||
|
||||
market_analyst:
|
||||
role: Market Analyst
|
||||
instructions: # Canonical |
|
||||
instructions: | # Canonical
|
||||
You analyze market trends and competitive landscapes.
|
||||
You provide actionable business insights.
|
||||
goal: Analyze market trends and opportunities
|
||||
|
||||
aggregator:
|
||||
role: Synthesizer
|
||||
instructions: # Canonical |
|
||||
instructions: | # Canonical
|
||||
You combine findings from multiple sources into coherent reports.
|
||||
You identify patterns and key insights.
|
||||
goal: Synthesize all findings into comprehensive reports
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# HeyGen + ElevenLabs Video Generation Workflow
|
||||
#
|
||||
# Sequential multi-agent workflow:
|
||||
# 1. Audio Agent → generates speech with ElevenLabs
|
||||
# 2. Upload Agent → uploads audio to HeyGen
|
||||
# 3. Video Agent → triggers HeyGen video generation
|
||||
# 4. Status Agent → polls and waits for video completion
|
||||
#
|
||||
# Usage:
|
||||
# python video_heygen_elevenlabs_workflow.py
|
||||
#
|
||||
# Required env vars:
|
||||
# HEYGEN_API_KEY, ELEVENLABS_API_KEY, OPENAI_API_KEY
|
||||
|
||||
framework: praisonai
|
||||
process: sequential
|
||||
|
||||
agents:
|
||||
audio_generator:
|
||||
role: Audio Generator
|
||||
goal: Generate speech audio from text using ElevenLabs
|
||||
instructions: |
|
||||
You generate speech audio files using ElevenLabs text-to-speech.
|
||||
When given text, call elevenlabs_speak_to_file with the text and output_path.
|
||||
IMPORTANT: Only pass 'text' and 'output_path' parameters. Use all defaults for other params.
|
||||
Report the output_path and audio_bytes from the result.
|
||||
tools:
|
||||
- elevenlabs_speak_to_file
|
||||
|
||||
asset_uploader:
|
||||
role: Asset Uploader
|
||||
goal: Upload audio files to HeyGen as assets
|
||||
instructions: |
|
||||
You upload audio files to HeyGen for video generation.
|
||||
Call heygen_upload_asset with the file_path and content_type="audio/mpeg".
|
||||
Report the asset "id" from the result - this is critical for the next step.
|
||||
tools:
|
||||
- heygen_upload_asset
|
||||
|
||||
video_generator:
|
||||
role: Video Generator
|
||||
goal: Generate HeyGen avatar video using uploaded audio
|
||||
instructions: |
|
||||
You create HeyGen avatar videos using external audio.
|
||||
Call heygen_generate_video_with_audio with the avatar_id and audio_asset_id.
|
||||
The audio_asset_id comes from the previous step's upload result.
|
||||
Report the video_id from the result data.
|
||||
tools:
|
||||
- heygen_generate_video_with_audio
|
||||
|
||||
video_monitor:
|
||||
role: Video Monitor
|
||||
goal: Wait for video completion and report the final URL
|
||||
instructions: |
|
||||
You monitor HeyGen video generation status.
|
||||
Call heygen_wait_for_video with the video_id from the previous step.
|
||||
Use poll_interval=10 and max_wait=300.
|
||||
When complete, report the video_url from the result.
|
||||
tools:
|
||||
- heygen_wait_for_video
|
||||
|
||||
steps:
|
||||
- agent: audio_generator
|
||||
action: |
|
||||
Generate speech audio for this text: "{{script}}"
|
||||
Save to output_path: "/tmp/workflow_speech.mp3"
|
||||
|
||||
- agent: asset_uploader
|
||||
action: |
|
||||
Upload the audio file at "/tmp/workflow_speech.mp3" to HeyGen.
|
||||
Use content_type "audio/mpeg".
|
||||
|
||||
- agent: video_generator
|
||||
action: |
|
||||
Generate a HeyGen video using:
|
||||
- avatar_id: "{{avatar_id}}"
|
||||
- audio_asset_id: use the asset id from the previous upload step
|
||||
|
||||
- agent: video_monitor
|
||||
action: |
|
||||
Wait for the video to complete. Use the video_id from the previous step.
|
||||
Poll every 10 seconds, max wait 300 seconds.
|
||||
Report the final video URL.
|
||||
|
||||
variables:
|
||||
script: "Welcome to PraisonAI! Build powerful AI agents in just three lines of code. Fast, simple, and production-ready."
|
||||
avatar_id: "YOUR_AVATAR_ID"
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the HeyGen + ElevenLabs YAML workflow.
|
||||
|
||||
This loads the YAML workflow and registers the praisonai_tools
|
||||
functions so the YAML parser can resolve them by name.
|
||||
|
||||
Usage:
|
||||
export HEYGEN_API_KEY=your_key
|
||||
export ELEVENLABS_API_KEY=your_key
|
||||
export OPENAI_API_KEY=your_key
|
||||
python video_heygen_elevenlabs_workflow.py
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from praisonai_tools import (
|
||||
elevenlabs_speak_to_file,
|
||||
heygen_upload_asset,
|
||||
heygen_generate_video_with_audio,
|
||||
heygen_wait_for_video,
|
||||
)
|
||||
from praisonaiagents.workflows.yaml_parser import YAMLWorkflowParser
|
||||
|
||||
# Register praisonai_tools functions for YAML tool resolution
|
||||
tool_registry = {
|
||||
"elevenlabs_speak_to_file": elevenlabs_speak_to_file,
|
||||
"heygen_upload_asset": heygen_upload_asset,
|
||||
"heygen_generate_video_with_audio": heygen_generate_video_with_audio,
|
||||
"heygen_wait_for_video": heygen_wait_for_video,
|
||||
}
|
||||
|
||||
# Parse YAML workflow
|
||||
yaml_path = Path(__file__).parent / "video_heygen_elevenlabs.yaml"
|
||||
parser = YAMLWorkflowParser(tool_registry=tool_registry)
|
||||
workflow = parser.parse_file(yaml_path)
|
||||
|
||||
print("=" * 60)
|
||||
print("HeyGen + ElevenLabs YAML Workflow")
|
||||
print("=" * 60)
|
||||
|
||||
# Run the workflow
|
||||
result = workflow.start()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("WORKFLOW RESULT:")
|
||||
print("=" * 60)
|
||||
print(result)
|
||||
@@ -0,0 +1,275 @@
|
||||
---
|
||||
description: Review, validate, and merge GitHub pull requests or triage issues
|
||||
---
|
||||
|
||||
# PR / Issue Review Workflow
|
||||
|
||||
When given a GitHub PR or Issue URL, follow this procedure end-to-end.
|
||||
|
||||
---
|
||||
|
||||
## Step 0 — Determine Type
|
||||
|
||||
Parse the URL to identify whether it is a **Pull Request** or an **Issue**.
|
||||
|
||||
```
|
||||
https://github.com/MervinPraison/PraisonAI/pull/<NUMBER> → PR
|
||||
https://github.com/MervinPraison/PraisonAI/issues/<NUMBER> → Issue
|
||||
```
|
||||
|
||||
- **PR** → go to Step 1
|
||||
- **Issue** → go to Step 8
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Fetch PR Metadata
|
||||
|
||||
// turbo
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package && \
|
||||
gh pr view <NUMBER> --json title,body,author,state,additions,deletions,changedFiles,headRefName,baseRefName,labels
|
||||
```
|
||||
|
||||
Record: title, author, branch, file count, +/- lines.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Read the Full Diff
|
||||
|
||||
// turbo
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package && gh pr diff <NUMBER>
|
||||
```
|
||||
|
||||
If the diff is very large (>500 lines), pipe through `head -500` first, then read remaining chunks.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Review the Diff
|
||||
|
||||
For each changed file, evaluate against **PraisonAI engineering principles** (from AGENTS.md):
|
||||
|
||||
| Principle | What to Check |
|
||||
|-----------|---------------|
|
||||
| **Protocol-driven core** | No heavy implementations in `praisonaiagents/` (core SDK) |
|
||||
| **Lazy imports** | Optional deps imported inside functions, not at module level |
|
||||
| **DRY** | No code duplication; reuse existing abstractions |
|
||||
| **Naming conventions** | `add_X()`, `get_X()`, `XConfig`, `XProtocol`, `XAdapter` |
|
||||
| **Async-safe** | No blocking I/O in async context; asyncio primitives used |
|
||||
| **Multi-agent safe** | No shared mutable global state without locks |
|
||||
| **Thread safety** | Double-checked locking for lazy caches; `threading.Lock` for mutables |
|
||||
| **Security** | No hardcoded secrets; `hmac.compare_digest` for comparisons; restricted `exec()` |
|
||||
| **Backward compat** | Public API changes require deprecation cycle |
|
||||
| **Test coverage** | Tests included; TDD pattern; both unit + agentic tests |
|
||||
|
||||
**Red flags to catch:**
|
||||
- Module-level imports of optional deps (chromadb, litellm, etc.)
|
||||
- Hardcoded credentials or `debug=True` in production paths
|
||||
- Direct string comparison for secrets (timing attacks)
|
||||
- `exec()`/`eval()` without sandbox
|
||||
- Shared mutable globals without `threading.Lock`
|
||||
- Missing error handling or swallowed exceptions
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Checkout and Run Tests Locally
|
||||
|
||||
// turbo
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package && git checkout main && git pull origin main
|
||||
```
|
||||
|
||||
// turbo
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package && gh pr checkout <NUMBER>
|
||||
```
|
||||
|
||||
Run targeted tests for changed files first:
|
||||
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package/src/praisonai-agents && \
|
||||
python -m pytest tests/ -x -q \
|
||||
--ignore=tests/integration/test_whatsapp_web_real.py \
|
||||
--ignore=tests/unit/tools/test_profiles.py \
|
||||
--tb=short 2>&1 | tail -20
|
||||
```
|
||||
|
||||
For PRs touching security-sensitive code, also run:
|
||||
|
||||
```bash
|
||||
# Sandbox tests
|
||||
python -m pytest tests/unit/tools/test_python_tools_sandbox.py -v
|
||||
|
||||
# CWE-78 env injection tests
|
||||
python -m pytest src/praisonai/tests/unit/test_cwe78_env_injection.py -v
|
||||
|
||||
# Thread-safety tests
|
||||
python -m pytest tests/unit/test_thread_safety.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Verify No Regressions
|
||||
|
||||
Run the full test suite:
|
||||
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package/src/praisonai-agents && \
|
||||
python -m pytest tests/ -q \
|
||||
--ignore=tests/integration/test_whatsapp_web_real.py \
|
||||
--ignore=tests/unit/tools/test_profiles.py \
|
||||
--tb=no 2>&1 | tail -5
|
||||
```
|
||||
|
||||
**Acceptable result:** All tests pass, or only pre-existing flaky tests fail (e.g., `test_file_memory.py::test_memory_persistence_across_agent_instances`).
|
||||
|
||||
**Unacceptable:** Any NEW failure introduced by the PR.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Approve or Request Changes
|
||||
|
||||
**If the PR is good:**
|
||||
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package && \
|
||||
gh pr review <NUMBER> --approve --body "LGTM — reviewed diff, tests pass, follows PraisonAI engineering principles."
|
||||
```
|
||||
|
||||
**If changes are needed:**
|
||||
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package && \
|
||||
gh pr review <NUMBER> --request-changes --body "Requesting changes: <specific feedback>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 7 — Merge
|
||||
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package && \
|
||||
gh pr merge <NUMBER> --merge --subject "<PR title>"
|
||||
```
|
||||
|
||||
Verify merge:
|
||||
|
||||
// turbo
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package && gh pr view <NUMBER> --json state,mergedAt
|
||||
```
|
||||
|
||||
Then return to main:
|
||||
|
||||
// turbo
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package && git checkout main && git pull origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 8 — Issue Triage (if URL is an Issue)
|
||||
|
||||
### 8a. Fetch issue details
|
||||
|
||||
// turbo
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package && \
|
||||
gh issue view <NUMBER> --json title,body,author,state,labels,comments
|
||||
```
|
||||
|
||||
### 8b. Classify severity
|
||||
|
||||
| Tier | Criteria | Examples |
|
||||
|------|----------|---------|
|
||||
| **Tier 1 — Critical** | Security vuln, data loss, crash in hot path | exec() without sandbox, hardcoded secrets, unprotected globals |
|
||||
| **Tier 2 — High** | Performance regression, broken feature, wrong output | Duplicate API calls, memory leak, silent failure |
|
||||
| **Tier 3 — Medium** | DX issue, missing validation, doc gap | Bad error messages, missing CLI parity, stale docs |
|
||||
| **Tier 4 — Low** | Cosmetic, code style, minor refactor | Trailing whitespace, naming inconsistency |
|
||||
|
||||
### 8c. Deep analysis (for Tier 1-2)
|
||||
|
||||
1. Read ALL relevant source files end-to-end
|
||||
2. Trace the code path that the issue describes
|
||||
3. Identify root cause with file paths and line numbers
|
||||
4. Assess blast radius (what else could be affected)
|
||||
5. Determine if the issue is **actually what it claims** (re-classify if needed)
|
||||
|
||||
### 8d. Delegate Implementation to GitHub Workflow Agents
|
||||
|
||||
> **KEY PHILOSOPHY: We act as the Manager, GitHub Actions execute the Workflow.**
|
||||
|
||||
**DO NOT implement the fix yourself locally.** PraisonAI uses autonomous GitHub Action workflows (such as `gemini-issue-review.yml` and `claude.yml`) to automatically resolve issues and create PRs. Your job as the local agent is to supervise, analyze gaps, and trigger these cloud agents to do the heavy lifting. If there is a gap (e.g. strict test violations), provide detailed relevant information in the PR/Issue and trigger the agents again.
|
||||
|
||||
1. Issue the command to trigger the cloud agent on the issue:
|
||||
```bash
|
||||
cd /Users/praison/praisonai-package && \
|
||||
gh issue comment <NUMBER> --body "@gemini please review and fix this issue"
|
||||
```
|
||||
*(Note: You can use `@claude` or `@gemini` depending on preference or active bots).*
|
||||
|
||||
2. Check the GitHub Actions logs (`gh run list`) and wait for the cloud agent to open a Pull Request (`gh pr list`).
|
||||
3. Once the cloud bot opens a Pull Request for the issue, return to **Step 1** to review the PR, test the branch locally, and merge it if it passes the checks.
|
||||
|
||||
---
|
||||
|
||||
## Step 9 — (Skipped) Local PR Creation
|
||||
|
||||
> **Note:** As the local agent, you should delegate code fixes to the cloud agents. Only implement code locally and create a PR if the USER explicitly forces you to bypass the cloud agents. If forced, use standard prefixes: `fix:`, `feat:`, `security:`, `refactor:`, `docs:`, `test:`.
|
||||
|
||||
---
|
||||
|
||||
## Step 10 — Security-Specific Checks
|
||||
|
||||
When reviewing security-related PRs, additionally verify:
|
||||
|
||||
### Credentials / Secrets
|
||||
```bash
|
||||
# Check for hardcoded secrets
|
||||
grep -rn "secret\|password\|token\|api_key" --include="*.py" <changed-files> | \
|
||||
grep -v "test_\|#\|environ\|getenv\|config"
|
||||
```
|
||||
|
||||
### Timing Attacks
|
||||
```bash
|
||||
# Ensure hmac.compare_digest for secret comparisons
|
||||
grep -rn "==.*api_key\|==.*secret\|==.*token" --include="*.py" <changed-files>
|
||||
```
|
||||
|
||||
### Authentication & Workflow Secrets
|
||||
When reviewing GitHub Actions workflows (especially ones using cloud actions like `claude-code-action`):
|
||||
- **OIDC Validation**: Any workflow using OIDC to authenticate with a cloud provider (e.g. AWS Bedrock for Claude, GCP for Vertex) **MUST** include `id-token: write` in the job's `permissions`. Without it, you will see `Could not fetch an OIDC token` errors.
|
||||
- **Valid PAT Usage**: When injecting tokens into workflows via `github_token:`, ensure you use `secrets.GH_TOKEN`. `secrets.PAT_TOKEN` has been known to be invalid or cause `401 Bad credentials` errors when curling GitHub resources. By design, `@copilot` mentions also strictly require `GH_TOKEN`.
|
||||
|
||||
### Supply Chain
|
||||
```bash
|
||||
# Check dependency versions
|
||||
pip show <package-name>
|
||||
# Check for known compromised versions
|
||||
pip install <package>==999 2>&1 | grep "from versions"
|
||||
```
|
||||
|
||||
### IoC Scanning
|
||||
```bash
|
||||
# Check for known malicious files
|
||||
find $(python -c "import site; print(site.getsitepackages()[0])") -name "<suspicious-file>" 2>/dev/null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference — Common Commands
|
||||
|
||||
| Action | Command |
|
||||
|--------|---------|
|
||||
| View PR | `gh pr view <N> --json title,body,state,additions,deletions` |
|
||||
| View diff | `gh pr diff <N>` |
|
||||
| Checkout PR | `gh pr checkout <N>` |
|
||||
| List open PRs | `gh pr list` |
|
||||
| List open issues | `gh issue list` |
|
||||
| View issue | `gh issue view <N>` |
|
||||
| Approve PR | `gh pr review <N> --approve` |
|
||||
| Request changes | `gh pr review <N> --request-changes --body "..."` |
|
||||
| Merge PR | `gh pr merge <N> --merge` |
|
||||
| Create PR | `gh pr create --title "..." --body "..." --head <branch> --base main` |
|
||||
| Run tests | `cd src/praisonai-agents && python -m pytest tests/ -q --tb=no` |
|
||||
| Check dep version | `pip show <package>` |
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
description: PR review chain — how automated reviews flow for every PR
|
||||
---
|
||||
|
||||
## Review Chain (Sequential)
|
||||
|
||||
> **KEY PHILOSOPHY: We act as the Manager, GitHub Actions execute the Workflow.**
|
||||
> As the local agent, our job is to analyze, supervise, and coordinate the cloud bots. If we identify a gap in the review chain or code (e.g., missing tests), provide detailed relevant information and trigger the agents (like `@claude`) again.
|
||||
```
|
||||
PR opened
|
||||
↓
|
||||
CodeRabbit (@coderabbitai) ─── auto for human PRs, triggered via comment for bot PRs
|
||||
Qodo (/review) ─── auto for human PRs, triggered via comment for bot PRs
|
||||
Gemini (@gemini) ─── triggered via comment for bot PRs/Issues
|
||||
↓ (~3-5 min) ↓ (auto triggers Claude on completion)
|
||||
Copilot (@copilot) ─── triggered ONLY after CodeRabbit or Qodo post their review
|
||||
↓
|
||||
Claude (@claude) ─── triggered ONLY after Copilot OR Gemini finishes (final reviewer)
|
||||
```
|
||||
|
||||
## Workflow Files
|
||||
|
||||
| File | Trigger | Does what |
|
||||
|------|---------|-----------|
|
||||
| `auto-pr-comment.yml` | `issue_comment`, `pull_request_review`, `pull_request:opened` | Triggers Copilot after CodeRabbit/Qodo finish. For bot PRs: triggers CodeRabbit+Qodo+Gemini first. |
|
||||
| `chain-claude-after-copilot.yml` | `pull_request_review:submitted`, `issue_comment` | Triggers Claude after Copilot reviews, AND automatically after Gemini Code Assist finishes fixing issues/PRs. |
|
||||
| `claude.yml` | `issue_comment`, `pull_request_review_comment`, `issues:assigned/labeled` | Claude responds to @claude mentions. |
|
||||
|
||||
## Bot PR fix
|
||||
|
||||
CodeRabbit and Qodo skip `github-actions[bot]` authored PRs by default. The `bot-pr-trigger-reviews` job in `auto-pr-comment.yml` explicitly triggers them via comments.
|
||||
|
||||
## Key rules
|
||||
|
||||
- **Copilot ignores bot comments.** All `@copilot` mentions MUST use `GH_TOKEN` (not `GITHUB_TOKEN`) so comments post as `MervinPraison`.
|
||||
- Copilot NEVER triggers before CodeRabbit/Qodo complete.
|
||||
- Claude NEVER triggers before Copilot completes.
|
||||
- Duplicate `@copilot` comments are prevented by checking existing comments.
|
||||
- PraisonAI PRs: NEVER merge automatically — user merges manually.
|
||||
|
||||
## Security & Credentials
|
||||
|
||||
- **OIDC Configuration (`id-token: write`)**: Any workflow utilizing the `anthropics/claude-code-action@beta` (or similar actions requiring JWTs for AWS Bedrock/GCP Vertex) MUST define `id-token: write` under the job's `permissions` block. Missing this causes deep, opaque failures like "Could not fetch an OIDC token" paired with `Bad credentials` execution limits.
|
||||
- **Valid `github_token` Payload**: Always pass `secrets.GH_TOKEN` into `github_token` inputs rather than `secrets.PAT_TOKEN`, as the latter has historically proven invalid and caused silent 401 exceptions on curl requests behind the scenes.
|
||||
@@ -93,4 +93,6 @@ chroma_db/
|
||||
|
||||
.praisonai
|
||||
.windsurf
|
||||
.agent
|
||||
.ruff_cache
|
||||
*.sfdx
|
||||
.sfdx/
|
||||
@@ -21,9 +21,11 @@ Simpler than competitors • More extensible • Faster • Agent-centric
|
||||
| **Minimal API** | Fewer parameters, sensible defaults, explicit overrides |
|
||||
| **Performance-First** | Lazy loading, optional dependencies, no hot-path regressions |
|
||||
| **Production-Ready** | Safe by default, multi-agent safe, async-safe |
|
||||
Powerful, lightweight, highly reliable and robust
|
||||
Easy and developer frienldy for non developers
|
||||
Easy and user friendly
|
||||
| **Open Core** | Core free and open source; clear paid upgrade path (support/cloud/services) |
|
||||
|
||||
Powerful, lightweight, highly reliable and robust.
|
||||
Easy and developer-friendly for non-developers.
|
||||
Each feature runs 3 ways: **CLI, YAML, Python**.
|
||||
---
|
||||
|
||||
## 2. Repository Structure
|
||||
@@ -96,22 +98,33 @@ Easy and user friendly
|
||||
|
||||
| Module | Size | Purpose |
|
||||
|--------|------|---------|
|
||||
| `agent/` | 1.4M | Agent class, protocols, handoff, autonomy |
|
||||
| `memory/` | 984K | Memory protocols, adapters, file memory |
|
||||
| `context/` | 960K | Context management, artifacts, fast context |
|
||||
| `tools/` | 732K | Tool SDK, decorators, registry, protocols |
|
||||
| `llm/` | 620K | LLM client, model router |
|
||||
| `workflows/` | 456K | Workflow engine, patterns (Route, Parallel, Loop) |
|
||||
| `knowledge/` | 436K | Knowledge/RAG protocols, vector stores |
|
||||
| `hooks/` | 260K | Hook system, middleware, events |
|
||||
| `eval/` | 240K | Evaluation framework |
|
||||
| `agent/` | 1.7M | Agent class, protocols, handoff, autonomy |
|
||||
| `memory/` | 784K | Memory protocols, adapters, file memory |
|
||||
| `context/` | 840K | Context management, artifacts, fast context |
|
||||
| `tools/` | 816K | Tool SDK, decorators, registry, protocols |
|
||||
| `llm/` | 756K | LLM client, model router |
|
||||
| `workflows/` | 472K | Workflow engine, patterns (Route, Parallel, Loop) |
|
||||
| `mcp/` | 404K | Model Context Protocol integration |
|
||||
| `eval/` | 384K | Evaluation framework |
|
||||
| `agents/` | 376K | Multi-agent orchestration (AgentTeam, AgentFlow) |
|
||||
| `knowledge/` | 300K | Knowledge/RAG protocols, vector stores |
|
||||
| `rag/` | 264K | RAG protocols, retriever, reranker |
|
||||
| `plugins/` | 56K | Plugin manager, registry |
|
||||
| `policy/` | 64K | Policy engine |
|
||||
| `config/` | 252K | Feature configs (ExecutionConfig, OutputConfig, etc.) |
|
||||
| `session/` | 224K | Session management, persistence |
|
||||
| `plugins/` | 204K | Plugin manager, registry |
|
||||
| `hooks/` | 184K | Hook system, middleware, events |
|
||||
| `storage/` | 136K | Storage protocols, adapters |
|
||||
| `scheduler/` | 132K | Schedule models, store, parser, runner |
|
||||
| `trace/` | 116K | Trace protocols, context events |
|
||||
| `approval/` | 92K | Human-in-the-loop approval protocols |
|
||||
| `bots/` | 80K | BotOS protocols, config |
|
||||
| `policy/` | 76K | Policy engine |
|
||||
| `gateway/` | 60K | Gateway protocols |
|
||||
| `sandbox/` | 56K | Code execution sandbox |
|
||||
| `bus/` | 48K | Event bus |
|
||||
| `streaming/` | 56K | Streaming events, callbacks |
|
||||
| `trace/` | 152K | Trace protocols, context events |
|
||||
| `scheduler/` | 48K | Schedule models, store, parser, runner |
|
||||
| `streaming/` | 44K | Streaming events, callbacks |
|
||||
| `conditions/` | 40K | Conditional logic protocols |
|
||||
| `db/` | 32K | Database adapter protocols |
|
||||
|
||||
### 3.2 Protocol-First Design
|
||||
|
||||
@@ -512,9 +525,9 @@ class MyDbAdapter(DbAdapter):
|
||||
|
||||
---
|
||||
|
||||
## 7. CLI Representation
|
||||
## 7. CLI & YAML Representation
|
||||
|
||||
Every feature must have CLI parity:
|
||||
Every feature must run 3 ways: **Python, CLI, and YAML**.
|
||||
|
||||
```bash
|
||||
# Basic agent
|
||||
@@ -636,6 +649,30 @@ pytest tests/unit/
|
||||
pytest --cov=praisonaiagents tests/
|
||||
```
|
||||
|
||||
### 9.4 Real Agentic Test (MANDATORY)
|
||||
|
||||
Every feature MUST include a real agentic test — not just smoke tests.
|
||||
|
||||
A **real agentic test** means the agent actually runs and calls the LLM:
|
||||
|
||||
```python
|
||||
# ✅ REAL agentic test — agent runs end-to-end
|
||||
from praisonaiagents import Agent
|
||||
agent = Agent(name="test", instructions="You are a helpful assistant")
|
||||
result = agent.start("Say hello in one sentence")
|
||||
print(result) # Must produce actual LLM output
|
||||
|
||||
# ❌ SMOKE test only — NOT sufficient alone
|
||||
agent = Agent(name="test") # Just object construction
|
||||
assert agent.name == "test" # No LLM call
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Both smoke AND real agentic tests are required
|
||||
- Agent MUST call `agent.start()` with a real prompt
|
||||
- Agent MUST call the LLM and produce a text response
|
||||
- Print full output so developers can verify end-to-end behavior
|
||||
|
||||
---
|
||||
|
||||
## 10. Implementation Checklist
|
||||
@@ -646,8 +683,10 @@ For every feature/change:
|
||||
- [ ] **No new deps**: Use optional dependencies only
|
||||
- [ ] **Lazy imports**: Heavy deps imported inside functions
|
||||
- [ ] **Naming**: Follow conventions (add_*, get_*, XConfig)
|
||||
- [ ] **Tests**: TDD - write failing tests first
|
||||
- [ ] **Tests**: TDD — write failing tests first
|
||||
- [ ] **Real agentic test**: Agent must call LLM end-to-end (§9.4)
|
||||
- [ ] **CLI**: Add corresponding CLI command/option
|
||||
- [ ] **YAML**: Ensure YAML config parity with Python API
|
||||
- [ ] **Docs**: Update Mintlify documentation
|
||||
- [ ] **Examples**: Add to examples/ directory
|
||||
- [ ] **Multi-agent safe**: No shared mutable state
|
||||
@@ -853,7 +892,7 @@ platforms:
|
||||
|
||||
---
|
||||
|
||||
## 13. Paid Value Path
|
||||
## 14. Paid Value Path
|
||||
|
||||
The core remains **free and open source**. Clear upgrade paths to paid value:
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
# Centralized Logging in PraisonAI Agents
|
||||
|
||||
This document explains the enhanced centralized logging system implemented to address inconsistent logging patterns across the codebase.
|
||||
|
||||
## Problem Addressed
|
||||
|
||||
- **53+ scattered logger instances** with no unified configuration
|
||||
- **No standard log format** across modules
|
||||
- **Mixed logging approaches** (logger.debug(), print(), console.print())
|
||||
- **No structured logging option** for production deployments
|
||||
- **Difficult filtering** by subsystem in multi-agent runs
|
||||
|
||||
## Solution
|
||||
|
||||
Enhanced the existing `praisonaiagents/_logging.py` with:
|
||||
|
||||
1. **Consistent naming convention**: All loggers follow `praisonaiagents.<module>` pattern
|
||||
2. **Structured logging support**: JSON output for production environments
|
||||
3. **Backward compatibility**: Existing code continues to work
|
||||
4. **Centralized configuration**: Single place to configure all logging
|
||||
5. **Optional extra data**: Structured data for better observability
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
# New recommended approach
|
||||
from praisonaiagents import get_logger
|
||||
|
||||
# Automatic module detection
|
||||
logger = get_logger()
|
||||
logger.info("This is a log message")
|
||||
|
||||
# Explicit module name
|
||||
logger = get_logger(__name__)
|
||||
logger.info("Explicit module logging")
|
||||
```
|
||||
|
||||
### Structured Logging
|
||||
|
||||
```python
|
||||
import os
|
||||
from praisonaiagents import get_logger, configure_structured_logging
|
||||
|
||||
# Enable structured JSON logging
|
||||
os.environ['PRAISONAI_STRUCTURED_LOGS'] = 'true'
|
||||
configure_structured_logging()
|
||||
|
||||
# Logger with extra structured data
|
||||
logger = get_logger(extra_data={"agent_id": "assistant", "session": "123"})
|
||||
logger.info("Task completed", extra={"task_id": "task_001", "duration": 1.5})
|
||||
```
|
||||
|
||||
Output:
|
||||
```json
|
||||
{
|
||||
"timestamp": "2026-03-30T21:35:00Z",
|
||||
"level": "INFO",
|
||||
"logger": "praisonaiagents.agent.agent",
|
||||
"message": "Task completed",
|
||||
"module": "agent",
|
||||
"function": "start",
|
||||
"line": 42,
|
||||
"agent_id": "assistant",
|
||||
"session": "123",
|
||||
"task_id": "task_001",
|
||||
"duration": 1.5
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `LOGLEVEL` | `WARNING` | Global log level (DEBUG, INFO, WARNING, ERROR) |
|
||||
| `PRAISONAI_STRUCTURED_LOGS` | `false` | Enable JSON structured logging |
|
||||
|
||||
### Per-Module Configuration
|
||||
|
||||
```bash
|
||||
# Set specific module to DEBUG level
|
||||
export LOGLEVEL=INFO
|
||||
# Configure in Python:
|
||||
logging.getLogger('praisonaiagents.memory').setLevel(logging.DEBUG)
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### For New Code
|
||||
|
||||
Use the new logging utilities:
|
||||
|
||||
```python
|
||||
# ✅ Recommended - new code
|
||||
from praisonaiagents import get_logger
|
||||
logger = get_logger()
|
||||
```
|
||||
|
||||
### For Existing Code
|
||||
|
||||
**No changes required** - existing code continues to work:
|
||||
|
||||
```python
|
||||
# ✅ Still works - existing code
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
```
|
||||
|
||||
**Optional enhancement** for better observability:
|
||||
|
||||
```python
|
||||
# ✅ Enhanced - gradually migrate
|
||||
from praisonaiagents._logging import get_logger
|
||||
logger = get_logger(__name__, extra_data={"subsystem": "agent"})
|
||||
```
|
||||
|
||||
## Naming Convention
|
||||
|
||||
The `get_logger()` function automatically ensures consistent naming:
|
||||
|
||||
| Input | Output Logger Name |
|
||||
|-------|--------------------|
|
||||
| `"my_module"` | `"praisonaiagents.my_module"` |
|
||||
| `"praisonaiagents.existing"` | `"praisonaiagents.existing"` (unchanged) |
|
||||
| `"praisonai.other"` | `"praisonaiagents.other"` |
|
||||
| `"__main__"` | `"praisonaiagents.main"` |
|
||||
| `None` (auto-detect) | `"praisonaiagents.<current_module>"` |
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### ELK Stack / Elasticsearch
|
||||
|
||||
```python
|
||||
import os
|
||||
from praisonaiagents import configure_structured_logging
|
||||
|
||||
# Enable JSON logging for log shipping
|
||||
os.environ['PRAISONAI_STRUCTURED_LOGS'] = 'true'
|
||||
configure_structured_logging()
|
||||
```
|
||||
|
||||
### CloudWatch / Splunk
|
||||
|
||||
```python
|
||||
import logging
|
||||
from praisonaiagents import get_logger, StructuredFormatter
|
||||
|
||||
# Custom handler with structured format
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(StructuredFormatter())
|
||||
|
||||
logger = get_logger()
|
||||
logger.addHandler(handler)
|
||||
```
|
||||
|
||||
### Filtering by Subsystem
|
||||
|
||||
With structured logging enabled, you can easily filter logs:
|
||||
|
||||
```bash
|
||||
# Filter by agent subsystem
|
||||
jq 'select(.logger | startswith("praisonaiagents.agent"))' logs.json
|
||||
|
||||
# Filter by specific agent
|
||||
jq 'select(.agent_id == "assistant")' logs.json
|
||||
|
||||
# Filter by session
|
||||
jq 'select(.session == "session_123")' logs.json
|
||||
```
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
- ✅ All existing `logging.getLogger(__name__)` calls continue to work
|
||||
- ✅ No performance impact on existing code
|
||||
- ✅ No breaking changes to public APIs
|
||||
- ✅ Gradual migration path available
|
||||
|
||||
## Examples
|
||||
|
||||
See the updated modules for examples:
|
||||
- `praisonaiagents/plugins/builtin/logging_plugin.py` - Logging plugin with extra data
|
||||
- `praisonaiagents/memory/memory.py` - Memory module with subsystem tagging
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Unified format**: All PraisonAI logs follow the same format
|
||||
2. **Better observability**: Structured data for monitoring and debugging
|
||||
3. **Easy filtering**: Filter by subsystem, agent, or session
|
||||
4. **Production ready**: JSON output for log aggregation systems
|
||||
5. **Zero breaking changes**: Existing code continues to work unchanged
|
||||
6. **Performance conscious**: No import-time overhead for normal usage
|
||||
@@ -0,0 +1,20 @@
|
||||
import cProfile
|
||||
import pstats
|
||||
import io
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Warm up
|
||||
for _ in range(100):
|
||||
_ = Agent(name="Test", llm="gpt-4o-mini", output="silent")
|
||||
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
for _ in range(5000):
|
||||
_ = Agent(name="Test", llm="gpt-4o-mini", output="silent")
|
||||
|
||||
pr.disable()
|
||||
s = io.StringIO()
|
||||
ps = pstats.Stats(pr, stream=s).sort_stats('cumtime')
|
||||
ps.print_stats(30)
|
||||
print(s.getvalue())
|
||||
@@ -0,0 +1,31 @@
|
||||
import cProfile
|
||||
import pstats
|
||||
import io
|
||||
import time
|
||||
from praisonaiagents import Agent
|
||||
|
||||
def func():
|
||||
return Agent(name='Test', llm='gpt-4o-mini', output="silent")
|
||||
|
||||
# Warm up to fill caches
|
||||
for _ in range(100):
|
||||
func()
|
||||
|
||||
start_time = time.perf_counter()
|
||||
for _ in range(1000):
|
||||
func()
|
||||
end_time = time.perf_counter()
|
||||
avg_time = (end_time - start_time) / 1000 * 1_000_000
|
||||
print(f"Time without profile: {avg_time:.2f} μs")
|
||||
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
for _ in range(1000):
|
||||
func()
|
||||
|
||||
pr.disable()
|
||||
s = io.StringIO()
|
||||
ps = pstats.Stats(pr, stream=s).sort_stats('cumtime')
|
||||
ps.print_stats(30)
|
||||
print(s.getvalue())
|
||||
@@ -59,18 +59,7 @@ _logging.configure_root_logger()
|
||||
# Import configuration (lightweight, no heavy deps)
|
||||
from . import _config
|
||||
|
||||
# Lightweight imports that don't trigger heavy dependency chains
|
||||
from .tools.tools import Tools
|
||||
from .tools.base import BaseTool, ToolResult, ToolValidationError, validate_tool
|
||||
from .tools.decorator import tool, FunctionTool
|
||||
from .tools.registry import get_registry, register_tool, get_tool, ToolRegistry
|
||||
# db and obs are lazy-loaded via __getattr__ for performance
|
||||
|
||||
# Sub-packages for organized imports (pa.config, pa.tools, etc.)
|
||||
# These enable: import praisonaiagents as pa; pa.config.MemoryConfig
|
||||
from . import config
|
||||
from . import tools
|
||||
# Note: db, obs, knowledge and mcp are lazy-loaded via __getattr__ due to heavy deps
|
||||
# Note: tools, config, memory, workflows, db, obs, knowledge and mcp are lazy-loaded via __getattr__ due to heavy deps
|
||||
|
||||
# Embedding API - LAZY LOADED via __getattr__ for performance
|
||||
# Supports: embedding, embeddings, aembedding, aembeddings, EmbeddingResult, get_dimensions
|
||||
@@ -125,6 +114,19 @@ def _get_lazy_cache():
|
||||
# ============================================================================
|
||||
|
||||
_LAZY_IMPORTS = {
|
||||
# Tools (moved from eager imports for lazy loading)
|
||||
'Tools': ('praisonaiagents.tools.tools', 'Tools'),
|
||||
'BaseTool': ('praisonaiagents.tools.base', 'BaseTool'),
|
||||
'ToolResult': ('praisonaiagents.tools.base', 'ToolResult'),
|
||||
'ToolValidationError': ('praisonaiagents.tools.base', 'ToolValidationError'),
|
||||
'validate_tool': ('praisonaiagents.tools.base', 'validate_tool'),
|
||||
'tool': ('praisonaiagents.tools.decorator', 'tool'),
|
||||
'FunctionTool': ('praisonaiagents.tools.decorator', 'FunctionTool'),
|
||||
'get_registry': ('praisonaiagents.tools.registry', 'get_registry'),
|
||||
'register_tool': ('praisonaiagents.tools.registry', 'register_tool'),
|
||||
'get_tool': ('praisonaiagents.tools.registry', 'get_tool'),
|
||||
'ToolRegistry': ('praisonaiagents.tools.registry', 'ToolRegistry'),
|
||||
|
||||
# Main display utilities (imports rich)
|
||||
'TaskOutput': ('praisonaiagents.main', 'TaskOutput'),
|
||||
'ReflectionOutput': ('praisonaiagents.main', 'ReflectionOutput'),
|
||||
@@ -179,9 +181,7 @@ _LAZY_IMPORTS = {
|
||||
'HandoffDepthError': ('praisonaiagents.agent.handoff', 'HandoffDepthError'),
|
||||
'HandoffTimeoutError': ('praisonaiagents.agent.handoff', 'HandoffTimeoutError'),
|
||||
|
||||
# Embedding API
|
||||
'embedding': ('praisonaiagents.embedding.embed', 'embedding'),
|
||||
'embeddings': ('praisonaiagents.embedding.embed', 'embedding'),
|
||||
# Embedding API (Note: embedding/embeddings handled in custom_handler to override subpackage)
|
||||
'aembedding': ('praisonaiagents.embedding.embed', 'aembedding'),
|
||||
'aembeddings': ('praisonaiagents.embedding.embed', 'aembedding'),
|
||||
'embed': ('praisonaiagents.embedding.embed', 'embed'),
|
||||
@@ -203,6 +203,9 @@ _LAZY_IMPORTS = {
|
||||
|
||||
# Agent classes
|
||||
'Agent': ('praisonaiagents.agent.agent', 'Agent'),
|
||||
'BudgetExceededError': ('praisonaiagents.agent.agent', 'BudgetExceededError'),
|
||||
'Heartbeat': ('praisonaiagents.agent.heartbeat', 'Heartbeat'),
|
||||
'HeartbeatConfig': ('praisonaiagents.agent.heartbeat', 'HeartbeatConfig'),
|
||||
'ImageAgent': ('praisonaiagents.agent.image_agent', 'ImageAgent'),
|
||||
'VideoAgent': ('praisonaiagents.agent.video_agent', 'VideoAgent'),
|
||||
'VideoConfig': ('praisonaiagents.agent.video_agent', 'VideoConfig'),
|
||||
@@ -489,8 +492,61 @@ _LAZY_IMPORTS = {
|
||||
'PraisonConfig': ('praisonaiagents.config.loader', 'PraisonConfig'),
|
||||
'PluginsConfig': ('praisonaiagents.config.loader', 'PluginsConfig'),
|
||||
'DefaultsConfig': ('praisonaiagents.config.loader', 'DefaultsConfig'),
|
||||
|
||||
# Centralized Logging Utilities
|
||||
'get_logger': ('praisonaiagents._logging', 'get_logger'),
|
||||
'configure_structured_logging': ('praisonaiagents._logging', 'configure_structured_logging'),
|
||||
'StructuredFormatter': ('praisonaiagents._logging', 'StructuredFormatter'),
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# SUBPACKAGE FUNCTION OVERRIDES
|
||||
# ============================================================================
|
||||
# Some subpackage names conflict with function names we want to export.
|
||||
# Override them here to return the function instead of the module.
|
||||
# ============================================================================
|
||||
|
||||
# Override 'embedding' and 'embeddings' at module level to prevent subpackage import
|
||||
# These need to be set after _LAZY_IMPORTS is defined but before __getattr__ is created
|
||||
def _get_embedding_func():
|
||||
"""Lazy getter for embedding function."""
|
||||
# Import with alias to avoid overwriting the module proxy
|
||||
from .embedding.embed import embedding as _embedding_func
|
||||
return _embedding_func
|
||||
|
||||
# Create lazy properties that override the submodule
|
||||
class _EmbeddingProxy:
|
||||
"""Proxy object that loads embedding function on first access."""
|
||||
def __init__(self):
|
||||
self._func = None
|
||||
|
||||
def _load(self):
|
||||
"""Load the actual embedding function if not already loaded."""
|
||||
if self._func is None:
|
||||
self._func = _get_embedding_func()
|
||||
return self._func
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self._load()(*args, **kwargs)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._load(), name)
|
||||
|
||||
@property
|
||||
def __wrapped__(self):
|
||||
"""Support for inspect.signature() and functools.wraps."""
|
||||
return self._load()
|
||||
|
||||
@property
|
||||
def __signature__(self):
|
||||
"""Support for inspect.signature()."""
|
||||
import inspect
|
||||
return inspect.signature(self._load())
|
||||
|
||||
def __repr__(self):
|
||||
return f"<lazy proxy for {self._load()!r}>"
|
||||
|
||||
_global_embedding_proxy = _EmbeddingProxy()
|
||||
|
||||
def _custom_handler(name, cache):
|
||||
"""Handle special cases that need custom logic."""
|
||||
@@ -502,39 +558,39 @@ def _custom_handler(name, cache):
|
||||
cache['AgentManager'] = value
|
||||
cache['Agents'] = value
|
||||
return value
|
||||
|
||||
# Task removed in v4.0.0 - use Task instead
|
||||
if name == "Task":
|
||||
raise ImportError(
|
||||
"Task has been removed in v4.0.0. Use Task instead.\n"
|
||||
"Migration: Replace 'from praisonaiagents import Task' with 'from praisonaiagents import Task'\n"
|
||||
"Task supports all Task features including action, handler, loop_over, etc."
|
||||
)
|
||||
|
||||
# Handle embedding specifically if missing from module dict due to submodule reload
|
||||
if name in ("embedding", "embeddings"):
|
||||
return _global_embedding_proxy
|
||||
|
||||
# Module imports (return the module itself)
|
||||
if name == 'tools':
|
||||
import importlib
|
||||
mod = importlib.import_module('.tools', 'praisonaiagents')
|
||||
cache['tools'] = mod
|
||||
return mod
|
||||
if name == 'config':
|
||||
import importlib
|
||||
mod = importlib.import_module('.config', 'praisonaiagents')
|
||||
cache['config'] = mod
|
||||
return mod
|
||||
if name == 'memory':
|
||||
import importlib
|
||||
return importlib.import_module('.memory', 'praisonaiagents')
|
||||
mod = importlib.import_module('.memory', 'praisonaiagents')
|
||||
cache['memory'] = mod
|
||||
return mod
|
||||
if name == 'workflows':
|
||||
import importlib
|
||||
return importlib.import_module('.workflows', 'praisonaiagents')
|
||||
mod = importlib.import_module('.workflows', 'praisonaiagents')
|
||||
cache['workflows'] = mod
|
||||
return mod
|
||||
|
||||
raise AttributeError(f"Not handled by custom_handler: {name}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SUBPACKAGE FUNCTION OVERRIDES
|
||||
# ============================================================================
|
||||
# Some subpackage names conflict with function names we want to export.
|
||||
# Override them here to return the function instead of the module.
|
||||
# ============================================================================
|
||||
|
||||
# Override 'embedding' to return the function, not the subpackage
|
||||
from .embedding.embed import embedding as _embedding_func
|
||||
embedding = _embedding_func
|
||||
|
||||
# Also provide embeddings alias
|
||||
embeddings = _embedding_func
|
||||
# Override the submodule with our function proxy
|
||||
embedding = _global_embedding_proxy
|
||||
embeddings = embedding # embeddings is an alias
|
||||
|
||||
|
||||
# Create the __getattr__ function using centralized utility
|
||||
@@ -542,7 +598,7 @@ __getattr__ = create_lazy_getattr_with_fallback(
|
||||
mapping=_LAZY_IMPORTS,
|
||||
module_name=__name__,
|
||||
cache=_lazy_cache,
|
||||
fallback_modules=['tools', 'memory', 'config', 'workflows'],
|
||||
fallback_modules=[], # Note: 'embedding' excluded to avoid conflict with embedding() function
|
||||
custom_handler=_custom_handler
|
||||
)
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ This module consolidates all logging configuration in one place to avoid duplica
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from typing import List
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
# ========================================================================
|
||||
# ENVIRONMENT CONFIGURATION
|
||||
@@ -161,5 +162,150 @@ def initialize_logging():
|
||||
# It will be called lazily when LLM class is instantiated
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# STRUCTURED LOGGING SUPPORT
|
||||
# ========================================================================
|
||||
class StructuredFormatter(logging.Formatter):
|
||||
"""JSON formatter for structured logging in production environments."""
|
||||
|
||||
_STANDARD_FIELDS: frozenset = frozenset({
|
||||
"timestamp", "level", "logger", "message",
|
||||
"module", "function", "line", "exc_info", "stack_info",
|
||||
})
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
"""Format log record as structured JSON."""
|
||||
log_data: Dict[str, Any] = {
|
||||
"timestamp": self.formatTime(record, self.datefmt),
|
||||
"level": record.levelname,
|
||||
"logger": record.name,
|
||||
"message": record.getMessage(),
|
||||
"module": record.module,
|
||||
"function": record.funcName,
|
||||
"line": record.lineno,
|
||||
}
|
||||
|
||||
# Include exception info if present
|
||||
if record.exc_info:
|
||||
log_data["exc_info"] = self.formatException(record.exc_info)
|
||||
if record.stack_info:
|
||||
log_data["stack_info"] = self.formatStack(record.stack_info)
|
||||
|
||||
# Merge extra fields without overwriting standard log fields
|
||||
if hasattr(record, 'extra_data'):
|
||||
extra = {k: v for k, v in record.extra_data.items() if k not in self._STANDARD_FIELDS}
|
||||
log_data.update(extra)
|
||||
|
||||
return json.dumps(log_data)
|
||||
|
||||
|
||||
def configure_structured_logging():
|
||||
"""Configure structured JSON logging for production environments.
|
||||
|
||||
Call this function to enable structured logging across all PraisonAI modules.
|
||||
Useful for log aggregation systems like ELK, Splunk, or CloudWatch.
|
||||
|
||||
Example:
|
||||
import os
|
||||
from praisonaiagents._logging import configure_structured_logging
|
||||
|
||||
# Enable structured logging
|
||||
os.environ['PRAISONAI_STRUCTURED_LOGS'] = 'true'
|
||||
configure_structured_logging()
|
||||
"""
|
||||
if os.environ.get('PRAISONAI_STRUCTURED_LOGS', '').lower() == 'true':
|
||||
structured_formatter = StructuredFormatter()
|
||||
# Reconfigure the root logger handler so propagating loggers get JSON output
|
||||
for handler in logging.getLogger().handlers:
|
||||
handler.setFormatter(structured_formatter)
|
||||
# Also reconfigure any praisonaiagents loggers that have their own handlers
|
||||
for logger_name in logging.Logger.manager.loggerDict:
|
||||
if logger_name.startswith('praisonaiagents.'):
|
||||
child_logger = logging.getLogger(logger_name)
|
||||
for handler in child_logger.handlers:
|
||||
handler.setFormatter(structured_formatter)
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# CONSISTENT LOGGER NAMING
|
||||
# ========================================================================
|
||||
class _ExtraDataAdapter(logging.LoggerAdapter):
|
||||
"""LoggerAdapter that injects extra_data into every log record."""
|
||||
|
||||
def process(self, msg: str, kwargs: Dict[str, Any]) -> tuple:
|
||||
kwargs.setdefault('extra', {})
|
||||
kwargs['extra']['extra_data'] = self.extra
|
||||
return msg, kwargs
|
||||
|
||||
|
||||
def get_logger(
|
||||
name: Optional[str] = None,
|
||||
*,
|
||||
extra_data: Optional[Dict[str, Any]] = None,
|
||||
) -> Union[logging.Logger, logging.LoggerAdapter]:
|
||||
"""Get a logger with consistent naming convention for PraisonAI modules.
|
||||
|
||||
This function ensures all loggers follow the 'praisonaiagents.<module>' pattern
|
||||
and provides optional structured logging support.
|
||||
|
||||
Args:
|
||||
name: Logger name. If None, uses the calling module's __name__.
|
||||
If it doesn't start with 'praisonaiagents.', the prefix is added.
|
||||
extra_data: Optional dict of extra data to include in all log records.
|
||||
|
||||
Returns:
|
||||
Logger instance with consistent naming and optional structured data.
|
||||
|
||||
Example:
|
||||
# In any module file:
|
||||
from praisonaiagents._logging import get_logger
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Or with automatic detection:
|
||||
logger = get_logger()
|
||||
|
||||
# With extra structured data:
|
||||
logger = get_logger(extra_data={"agent_id": "assistant", "session": "123"})
|
||||
"""
|
||||
import inspect
|
||||
|
||||
# Auto-detect module name if not provided
|
||||
if name is None:
|
||||
frame = inspect.currentframe()
|
||||
try:
|
||||
caller_frame = frame.f_back
|
||||
name = caller_frame.f_globals.get('__name__', 'unknown')
|
||||
finally:
|
||||
del caller_frame # avoid reference cycle / frame leak
|
||||
del frame
|
||||
|
||||
# Ensure consistent naming convention
|
||||
if not name.startswith('praisonaiagents.'):
|
||||
if name == '__main__':
|
||||
name = 'praisonaiagents.main'
|
||||
elif name == 'praisonai' or name.startswith('praisonai.'):
|
||||
# Handle cases like 'praisonai.something' -> 'praisonaiagents.something'
|
||||
name = 'praisonaiagents' + name[len('praisonai'):]
|
||||
else:
|
||||
# Add prefix for non-praisonai modules
|
||||
name = f'praisonaiagents.{name}'
|
||||
|
||||
base_logger = logging.getLogger(name)
|
||||
|
||||
# Wrap with adapter when extra structured data is requested
|
||||
if extra_data:
|
||||
return _ExtraDataAdapter(base_logger, extra_data)
|
||||
|
||||
return base_logger
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# BACKWARD COMPATIBILITY ALIASES
|
||||
# ========================================================================
|
||||
# These maintain compatibility with existing code
|
||||
getLogger = get_logger # Snake case alias
|
||||
get_praisonai_logger = get_logger # Descriptive alias
|
||||
|
||||
|
||||
# Auto-initialize on import (but NOT litellm)
|
||||
initialize_logging()
|
||||
@@ -1,4 +1,12 @@
|
||||
"""Agent module for AI agents - uses lazy loading for performance"""
|
||||
"""Agent module for AI agents - uses lazy loading for performance
|
||||
|
||||
The Agent class has been decomposed into focused modules for better maintainability:
|
||||
- tool_execution: Tool calling and execution functionality
|
||||
- chat_handler: Chat and conversation management
|
||||
- session_manager: Session persistence and state management
|
||||
|
||||
Backward compatibility is maintained via mixins in the main Agent class.
|
||||
"""
|
||||
|
||||
# Lazy loading cache
|
||||
_lazy_cache = {}
|
||||
@@ -13,6 +21,18 @@ def __getattr__(name):
|
||||
from .agent import Agent
|
||||
_lazy_cache[name] = Agent
|
||||
return Agent
|
||||
if name == 'BudgetExceededError':
|
||||
from .agent import BudgetExceededError
|
||||
_lazy_cache[name] = BudgetExceededError
|
||||
return BudgetExceededError
|
||||
if name == 'Heartbeat':
|
||||
from .heartbeat import Heartbeat
|
||||
_lazy_cache[name] = Heartbeat
|
||||
return Heartbeat
|
||||
if name == 'HeartbeatConfig':
|
||||
from .heartbeat import HeartbeatConfig
|
||||
_lazy_cache[name] = HeartbeatConfig
|
||||
return HeartbeatConfig
|
||||
|
||||
# Specialized agents - lazy loaded (import rich)
|
||||
if name == 'ImageAgent':
|
||||
@@ -141,10 +161,39 @@ def __getattr__(name):
|
||||
_lazy_cache[name] = value
|
||||
return value
|
||||
|
||||
# Decomposed mixins - for advanced use cases
|
||||
if name == 'ToolExecutionMixin':
|
||||
from .tool_execution import ToolExecutionMixin
|
||||
_lazy_cache[name] = ToolExecutionMixin
|
||||
return ToolExecutionMixin
|
||||
elif name == 'ChatHandlerMixin':
|
||||
from .chat_handler import ChatHandlerMixin
|
||||
_lazy_cache[name] = ChatHandlerMixin
|
||||
return ChatHandlerMixin
|
||||
elif name == 'SessionManagerMixin':
|
||||
from .session_manager import SessionManagerMixin
|
||||
_lazy_cache[name] = SessionManagerMixin
|
||||
return SessionManagerMixin
|
||||
elif name == 'ChatMixin':
|
||||
from .chat_mixin import ChatMixin
|
||||
_lazy_cache[name] = ChatMixin
|
||||
return ChatMixin
|
||||
elif name == 'ExecutionMixin':
|
||||
from .execution_mixin import ExecutionMixin
|
||||
_lazy_cache[name] = ExecutionMixin
|
||||
return ExecutionMixin
|
||||
elif name == 'MemoryMixin':
|
||||
from .memory_mixin import MemoryMixin
|
||||
_lazy_cache[name] = MemoryMixin
|
||||
return MemoryMixin
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
__all__ = [
|
||||
'Agent',
|
||||
'BudgetExceededError',
|
||||
'Heartbeat',
|
||||
'HeartbeatConfig',
|
||||
'ImageAgent',
|
||||
'VideoAgent',
|
||||
'VideoConfig',
|
||||
@@ -198,4 +247,11 @@ __all__ = [
|
||||
'MemoryAwareAgentProtocol',
|
||||
'FullAgentProtocol',
|
||||
'ContextEngineerProtocol',
|
||||
# Decomposed mixins (for advanced use cases)
|
||||
'ToolExecutionMixin',
|
||||
'ChatHandlerMixin',
|
||||
'SessionManagerMixin',
|
||||
'ChatMixin',
|
||||
'ExecutionMixin',
|
||||
'MemoryMixin',
|
||||
]
|
||||
@@ -9,6 +9,7 @@ Follows the Agent() class patterns:
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
import warnings
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Any, Dict, Union, BinaryIO
|
||||
@@ -17,7 +18,6 @@ from pathlib import Path
|
||||
# Filter out Pydantic warning about fields
|
||||
warnings.filterwarnings("ignore", "Valid config keys have changed in V2", UserWarning)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# AudioConfig - Configuration dataclass following feature_configs.py patterns
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -59,7 +59,6 @@ class AudioConfig:
|
||||
"api_key": self.api_key,
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# AudioAgent Class - Agent-centric audio processing
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -192,10 +191,10 @@ class AudioAgent:
|
||||
def _configure_logging(self, verbose: Union[bool, int]) -> None:
|
||||
"""Configure logging levels."""
|
||||
if isinstance(verbose, int) and verbose >= 10:
|
||||
logging.getLogger("litellm").setLevel(logging.DEBUG)
|
||||
get_logger("litellm").setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.getLogger("litellm").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
get_logger("litellm").setLevel(logging.WARNING)
|
||||
get_logger("httpx").setLevel(logging.WARNING)
|
||||
|
||||
def _get_model_params(self, model: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Build parameters for LiteLLM calls."""
|
||||
|
||||
@@ -35,8 +35,9 @@ from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict, Any, List, Set
|
||||
from enum import Enum
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# ============================================================================
|
||||
# Unified Stage Enum (G-DUP-1 fix: single source of truth)
|
||||
@@ -72,7 +73,6 @@ _ESCALATION_TO_AUTONOMY_SIGNAL = {
|
||||
# Reverse map for converting autonomy signal strings back to EscalationSignal values
|
||||
_AUTONOMY_TO_ESCALATION_SIGNAL = {v: k for k, v in _ESCALATION_TO_AUTONOMY_SIGNAL.items()}
|
||||
|
||||
|
||||
# Valid autonomy modes
|
||||
VALID_AUTONOMY_MODES = {"caller", "iterative"}
|
||||
|
||||
@@ -83,7 +83,6 @@ _LEVEL_TO_DEFAULT_MODE = {
|
||||
"full_auto": "iterative",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class AutonomyConfig:
|
||||
"""Configuration for Agent autonomy features.
|
||||
@@ -196,7 +195,6 @@ class AutonomyConfig:
|
||||
default_tools=data.get("default_tools"),
|
||||
)
|
||||
|
||||
|
||||
class AutonomySignal(str, Enum):
|
||||
"""Signals detected from prompts for autonomy decisions.
|
||||
|
||||
@@ -215,26 +213,27 @@ class AutonomySignal(str, Enum):
|
||||
COMPLEX_KEYWORDS = "complex_keywords"
|
||||
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
import warnings
|
||||
warnings.warn(
|
||||
"AutonomySignal is deprecated. Use EscalationSignal instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
from ..utils.deprecation import warn_deprecated_param
|
||||
warn_deprecated_param(
|
||||
"AutonomySignal class",
|
||||
since="1.0.0",
|
||||
removal="2.0.0",
|
||||
alternative="use EscalationSignal from praisonaiagents.escalation.types instead",
|
||||
stacklevel=3
|
||||
)
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
|
||||
def _warn_autonomy_signal():
|
||||
"""Emit deprecation warning when AutonomySignal is accessed."""
|
||||
import warnings
|
||||
warnings.warn(
|
||||
"AutonomySignal is deprecated. Use EscalationSignal from "
|
||||
"praisonaiagents.escalation.types instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
from ..utils.deprecation import warn_deprecated_param
|
||||
warn_deprecated_param(
|
||||
"AutonomySignal",
|
||||
since="1.0.0",
|
||||
removal="2.0.0",
|
||||
alternative="use EscalationSignal from praisonaiagents.escalation.types instead",
|
||||
stacklevel=4
|
||||
)
|
||||
|
||||
|
||||
class AutonomyTrigger:
|
||||
"""Detects signals from prompts for autonomy decisions.
|
||||
|
||||
@@ -280,7 +279,6 @@ class AutonomyTrigger:
|
||||
pass
|
||||
return self._delegate.recommend_stage(esc_signals)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AutonomyResult:
|
||||
"""Result of an autonomous execution.
|
||||
@@ -315,7 +313,6 @@ class AutonomyResult:
|
||||
"""
|
||||
return self.output or ""
|
||||
|
||||
|
||||
class DoomLoopTracker:
|
||||
"""Tracks actions to detect doom loops with graduated recovery.
|
||||
|
||||
@@ -418,7 +415,6 @@ class DoomLoopTracker:
|
||||
self._delegate.start_session()
|
||||
self._recovery_attempts = 0
|
||||
|
||||
|
||||
class AutonomyMixin:
|
||||
"""Helper-only trait for autonomy utilities.
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Chat handler mixin for the Agent class.
|
||||
|
||||
Contains chat management, response processing, and message formatting.
|
||||
Extracted from agent.py for better modularity and maintainability.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional, Any, Dict, List, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChatHandlerMixin:
|
||||
"""Mixin providing chat handling methods for the Agent class.
|
||||
|
||||
This mixin focuses on high-level chat coordination and response processing,
|
||||
while the actual LLM interaction is handled by ChatMixin.
|
||||
"""
|
||||
|
||||
def _format_chat_response(self, response: str, metadata: Optional[Dict[str, Any]] = None) -> str:
|
||||
"""Format chat response with any necessary post-processing.
|
||||
|
||||
Args:
|
||||
response: Raw response from the LLM
|
||||
metadata: Optional metadata about the response
|
||||
|
||||
Returns:
|
||||
Formatted response string
|
||||
"""
|
||||
if not response:
|
||||
return ""
|
||||
|
||||
# Basic response formatting - can be extended with additional logic
|
||||
formatted = response.strip()
|
||||
|
||||
# Add any metadata-based formatting if needed
|
||||
if metadata and metadata.get('add_timestamp'):
|
||||
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
formatted = f"[{timestamp}] {formatted}"
|
||||
|
||||
return formatted
|
||||
|
||||
def _validate_chat_input(self, prompt: str) -> bool:
|
||||
"""Validate chat input before processing.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt to validate
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
if not prompt or not isinstance(prompt, str):
|
||||
logger.warning("Invalid chat input: prompt must be a non-empty string")
|
||||
return False
|
||||
|
||||
if len(prompt.strip()) == 0:
|
||||
logger.warning("Invalid chat input: prompt cannot be empty or whitespace only")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _prepare_chat_context(self, prompt: str, **kwargs) -> Dict[str, Any]:
|
||||
"""Prepare context for chat processing.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt
|
||||
**kwargs: Additional context parameters
|
||||
|
||||
Returns:
|
||||
Context dictionary for chat processing
|
||||
"""
|
||||
context = {
|
||||
'prompt': prompt,
|
||||
'timestamp': time.time(),
|
||||
'agent_name': getattr(self, 'name', None) or 'Agent',
|
||||
}
|
||||
|
||||
# Add any additional context from kwargs
|
||||
context.update(kwargs)
|
||||
|
||||
return context
|
||||
|
||||
def _handle_chat_error(self, error: Exception, prompt: str) -> str:
|
||||
"""Handle chat processing errors gracefully.
|
||||
|
||||
Args:
|
||||
error: The exception that occurred
|
||||
prompt: The original prompt that caused the error
|
||||
|
||||
Returns:
|
||||
Error response message
|
||||
"""
|
||||
logger.error(f"Chat error occurred: {error}", exc_info=True)
|
||||
|
||||
# Return a user-friendly error message without exposing internal details.
|
||||
# Include a timestamp reference so users can correlate with server logs.
|
||||
error_ref = time.strftime('%Y%m%d%H%M%S')
|
||||
return f"I encountered an error while processing your request (ref: {error_ref}). Please try again."
|
||||
|
||||
def _log_chat_interaction(self, prompt: str, response: str, duration: Optional[float] = None):
|
||||
"""Log chat interaction for debugging and analytics.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt
|
||||
response: Generated response
|
||||
duration: Processing duration in seconds
|
||||
"""
|
||||
agent_name = getattr(self, 'name', None) or 'Agent'
|
||||
duration_str = f" (took {duration:.2f}s)" if duration else ""
|
||||
|
||||
logger.debug(f"[{agent_name}] Chat interaction{duration_str}")
|
||||
logger.debug(f"[{agent_name}] Prompt: {prompt[:100]}{'...' if len(prompt) > 100 else ''}")
|
||||
logger.debug(f"[{agent_name}] Response: {response[:100]}{'...' if len(response) > 100 else ''}")
|
||||
@@ -4,7 +4,7 @@ from dataclasses import dataclass, field, asdict
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from praisonaiagents._logging import get_logger
|
||||
|
||||
@dataclass
|
||||
class CodeConfig:
|
||||
@@ -29,7 +29,6 @@ class CodeConfig:
|
||||
"""Convert config to dictionary."""
|
||||
return {k: v for k, v in asdict(self).items() if v is not None}
|
||||
|
||||
|
||||
class CodeAgent:
|
||||
"""Agent for code generation, execution, review, and refactoring.
|
||||
|
||||
@@ -96,7 +95,7 @@ class CodeAgent:
|
||||
self._console = None
|
||||
|
||||
# Configure logging
|
||||
self._logger = logging.getLogger(f"CodeAgent.{name}")
|
||||
self._logger = get_logger(f"CodeAgent.{name}")
|
||||
if not verbose:
|
||||
self._logger.setLevel(logging.WARNING)
|
||||
|
||||
@@ -309,8 +308,9 @@ Follow best practices and coding standards."""
|
||||
# Clean up temp file
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
# Temp file cleanup failed - not critical
|
||||
get_logger(__name__).debug(f"Failed to clean up temp file {temp_file}: {e}")
|
||||
|
||||
async def aexecute(self, code: str, language: str = "python", **kwargs) -> Dict[str, Any]:
|
||||
"""Execute code asynchronously.
|
||||
|
||||
@@ -14,6 +14,7 @@ This class implements proper Context Engineering principles following the PRD te
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, Any, Dict, Union, List, TYPE_CHECKING
|
||||
@@ -94,7 +95,6 @@ async def _async_subprocess_run(cmd: list, timeout: int = 60) -> tuple:
|
||||
from ..agent.agent import Agent
|
||||
from ..task import Task
|
||||
|
||||
|
||||
class ContextAgent(Agent):
|
||||
"""
|
||||
Advanced Context Engineering Agent - Comprehensive context generation for AI coding assistants.
|
||||
@@ -207,7 +207,7 @@ class ContextAgent(Agent):
|
||||
"""Setup comprehensive logging based on debug mode."""
|
||||
try:
|
||||
# Create logger
|
||||
self.logger = logging.getLogger(f"ContextAgent_{id(self)}")
|
||||
self.logger = get_logger(f"ContextAgent_{id(self)}")
|
||||
self.logger.setLevel(logging.DEBUG if self.debug_mode else logging.INFO)
|
||||
|
||||
# Clear existing handlers
|
||||
@@ -247,7 +247,7 @@ class ContextAgent(Agent):
|
||||
except Exception as e:
|
||||
print(f"⚠️ Warning: Could not setup logging: {e}")
|
||||
# Create a minimal logger as fallback
|
||||
self.logger = logging.getLogger(f"ContextAgent_{id(self)}")
|
||||
self.logger = get_logger(f"ContextAgent_{id(self)}")
|
||||
self.logger.setLevel(logging.INFO)
|
||||
|
||||
def setup_output_directories(self):
|
||||
@@ -2445,7 +2445,7 @@ This PRP provides:
|
||||
"""
|
||||
asyncio = _get_asyncio()
|
||||
# Run sync method in executor for now (subprocess calls are blocking)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
return await loop.run_in_executor(
|
||||
None,
|
||||
self.analyze_codebase_with_gitingest,
|
||||
@@ -2461,7 +2461,7 @@ This PRP provides:
|
||||
Async version of generate_prp.
|
||||
"""
|
||||
asyncio = _get_asyncio()
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
return await loop.run_in_executor(
|
||||
None,
|
||||
lambda: self.generate_comprehensive_prp(feature_request, context_analysis)
|
||||
@@ -2476,13 +2476,12 @@ This PRP provides:
|
||||
Async version of create_implementation_blueprint.
|
||||
"""
|
||||
asyncio = _get_asyncio()
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
return await loop.run_in_executor(
|
||||
None,
|
||||
lambda: self.build_implementation_blueprint(feature_request, context_analysis)
|
||||
)
|
||||
|
||||
|
||||
def create_context_agent(llm: Optional[Union[str, Any]] = None, **kwargs) -> ContextAgent:
|
||||
"""
|
||||
Factory function to create a ContextAgent following Context Engineering and PRD methodology.
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
"""Cost persistence — saves agent cost_summary to disk after runs.
|
||||
|
||||
Writes JSON files to ``~/.praisonai/costs/`` for CLI dashboard consumption.
|
||||
Zero overhead when not used (this module is never imported unless explicitly called).
|
||||
|
||||
Usage::
|
||||
|
||||
from praisonaiagents.agent.cost_persistence import save_cost_report, load_cost_report
|
||||
|
||||
save_cost_report(agent, session_name="my_run")
|
||||
report = load_cost_report("my_run")
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
COST_DIR = Path.home() / ".praisonai" / "costs"
|
||||
|
||||
def save_cost_report(
|
||||
agent,
|
||||
session_name: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Persist an agent's cost_summary to disk.
|
||||
|
||||
Args:
|
||||
agent: Agent instance with cost_summary property.
|
||||
session_name: Optional session identifier. Auto-generated if None.
|
||||
|
||||
Returns:
|
||||
Path to the saved JSON file.
|
||||
"""
|
||||
COST_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
name = session_name or f"{agent.name}_{int(time.time())}"
|
||||
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in name)
|
||||
|
||||
summary = getattr(agent, "cost_summary", None)
|
||||
if summary is None:
|
||||
summary = {
|
||||
"tokens_in": 0,
|
||||
"tokens_out": 0,
|
||||
"cost": 0.0,
|
||||
"llm_calls": 0,
|
||||
}
|
||||
|
||||
report = {
|
||||
"session": name,
|
||||
"agent_name": getattr(agent, "name", "unknown"),
|
||||
"timestamp": time.time(),
|
||||
"iso_time": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
**summary,
|
||||
}
|
||||
|
||||
filepath = COST_DIR / f"{safe_name}.json"
|
||||
with open(filepath, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
|
||||
logger.info(f"[costs] Saved cost report to {filepath}")
|
||||
return str(filepath)
|
||||
|
||||
def load_cost_report(session_name: str) -> Optional[Dict[str, Any]]:
|
||||
"""Load a cost report by session name.
|
||||
|
||||
Args:
|
||||
session_name: Session identifier.
|
||||
|
||||
Returns:
|
||||
Parsed JSON dict, or None if not found.
|
||||
"""
|
||||
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in session_name)
|
||||
filepath = COST_DIR / f"{safe_name}.json"
|
||||
if filepath.exists():
|
||||
with open(filepath) as f:
|
||||
return json.load(f)
|
||||
return None
|
||||
|
||||
def list_cost_reports(limit: int = 20) -> List[Dict[str, Any]]:
|
||||
"""List recent cost reports, sorted by timestamp (newest first).
|
||||
|
||||
Args:
|
||||
limit: Max number of reports to return.
|
||||
|
||||
Returns:
|
||||
List of parsed JSON report dicts.
|
||||
"""
|
||||
if not COST_DIR.exists():
|
||||
return []
|
||||
|
||||
reports = []
|
||||
for fpath in sorted(COST_DIR.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
|
||||
try:
|
||||
with open(fpath) as f:
|
||||
reports.append(json.load(f))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
continue
|
||||
if len(reports) >= limit:
|
||||
break
|
||||
return reports
|
||||
|
||||
def format_cost_table(reports: List[Dict[str, Any]], as_json: bool = False) -> str:
|
||||
"""Format cost reports for terminal display.
|
||||
|
||||
Args:
|
||||
reports: List of report dicts from list_cost_reports().
|
||||
as_json: If True, returns JSON string instead of table.
|
||||
|
||||
Returns:
|
||||
Formatted string for terminal output.
|
||||
"""
|
||||
if as_json:
|
||||
return json.dumps(reports, indent=2)
|
||||
|
||||
if not reports:
|
||||
return "No cost reports found."
|
||||
|
||||
lines = []
|
||||
lines.append(f"{'Session':<30} {'Agent':<20} {'Cost ($)':<12} {'Tokens In':<12} {'Tokens Out':<12} {'LLM Calls':<10}")
|
||||
lines.append("-" * 96)
|
||||
for r in reports:
|
||||
lines.append(
|
||||
f"{r.get('session', '?'):<30} "
|
||||
f"{r.get('agent_name', '?'):<20} "
|
||||
f"${r.get('cost', 0):<11.4f} "
|
||||
f"{r.get('tokens_in', 0):<12} "
|
||||
f"{r.get('tokens_out', 0):<12} "
|
||||
f"{r.get('llm_calls', 0):<10}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
@@ -35,13 +35,13 @@ Example:
|
||||
|
||||
import os
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
import time
|
||||
import asyncio
|
||||
from typing import List, Optional, Any, Dict, Literal
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
|
||||
|
||||
# Data classes for Deep Research response types
|
||||
@dataclass
|
||||
class Citation:
|
||||
@@ -54,7 +54,6 @@ class Citation:
|
||||
def __repr__(self):
|
||||
return f"Citation(title='{self.title}', url='{self.url}')"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReasoningStep:
|
||||
"""Represents a reasoning step in the research process."""
|
||||
@@ -64,7 +63,6 @@ class ReasoningStep:
|
||||
def __repr__(self):
|
||||
return f"ReasoningStep(text='{self.text[:50]}...')" if len(self.text) > 50 else f"ReasoningStep(text='{self.text}')"
|
||||
|
||||
|
||||
@dataclass
|
||||
class WebSearchCall:
|
||||
"""Represents a web search call made during research."""
|
||||
@@ -74,7 +72,6 @@ class WebSearchCall:
|
||||
def __repr__(self):
|
||||
return f"WebSearchCall(query='{self.query}', status='{self.status}')"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CodeExecutionStep:
|
||||
"""Represents a code execution step during research."""
|
||||
@@ -84,7 +81,6 @@ class CodeExecutionStep:
|
||||
def __repr__(self):
|
||||
return f"CodeExecutionStep(input='{self.input_code[:50]}...')" if len(self.input_code) > 50 else f"CodeExecutionStep(input='{self.input_code}')"
|
||||
|
||||
|
||||
@dataclass
|
||||
class MCPCall:
|
||||
"""Represents an MCP tool call during research."""
|
||||
@@ -95,7 +91,6 @@ class MCPCall:
|
||||
def __repr__(self):
|
||||
return f"MCPCall(name='{self.name}', server='{self.server_label}')"
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileSearchCall:
|
||||
"""Represents a file search call (Gemini-specific)."""
|
||||
@@ -104,7 +99,6 @@ class FileSearchCall:
|
||||
def __repr__(self):
|
||||
return f"FileSearchCall(stores={self.store_names})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeepResearchResponse:
|
||||
"""
|
||||
@@ -149,14 +143,12 @@ class DeepResearchResponse:
|
||||
sources.append({"title": c.title, "url": c.url})
|
||||
return sources
|
||||
|
||||
|
||||
class Provider(Enum):
|
||||
"""Supported Deep Research providers."""
|
||||
OPENAI = "openai"
|
||||
GEMINI = "gemini"
|
||||
LITELLM = "litellm"
|
||||
|
||||
|
||||
class DeepResearchAgent:
|
||||
"""
|
||||
Agent for performing deep research using multiple provider APIs.
|
||||
@@ -295,7 +287,7 @@ You are a professional research analyst. When conducting research:
|
||||
self._gemini_client = None
|
||||
|
||||
# Configure logging
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger = get_logger(__name__)
|
||||
|
||||
if self.verbose:
|
||||
self.logger.debug(f"DeepResearchAgent initialized: provider={self.provider.value}, model={self.model}")
|
||||
@@ -868,6 +860,78 @@ You are a professional research analyst. When conducting research:
|
||||
|
||||
return self._parse_gemini_response(interaction)
|
||||
|
||||
async def _research_gemini_async(
|
||||
self,
|
||||
query: str,
|
||||
instructions: Optional[str] = None,
|
||||
model: Optional[str] = None,
|
||||
file_search: bool = False,
|
||||
file_search_stores: Optional[List[Dict[str, Any]]] = None,
|
||||
grounding_file_prompt: Optional[str] = None,
|
||||
) -> DeepResearchResponse:
|
||||
"""
|
||||
Async version of _research_gemini with non-blocking polling.
|
||||
Uses asyncio.sleep() to prevent blocking the event loop.
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
if self.provider != Provider.GEMINI:
|
||||
raise ValueError("This method requires GEMINI provider")
|
||||
|
||||
if not self.gemini_client:
|
||||
raise RuntimeError("Gemini client is required for Deep Research")
|
||||
|
||||
create_params = {
|
||||
"query": query,
|
||||
"file_search": file_search,
|
||||
}
|
||||
|
||||
if instructions:
|
||||
create_params["instructions"] = instructions
|
||||
|
||||
if model:
|
||||
create_params["model"] = model
|
||||
|
||||
if file_search_stores:
|
||||
create_params["file_search_stores"] = file_search_stores
|
||||
|
||||
if grounding_file_prompt:
|
||||
create_params["grounding_file_prompt"] = grounding_file_prompt
|
||||
|
||||
interaction = self.gemini_client.interactions.create(**create_params)
|
||||
|
||||
if self.verbose:
|
||||
self.logger.debug(f"Gemini research started (async): {interaction.id}")
|
||||
|
||||
start_time = time.time()
|
||||
while True:
|
||||
interaction = self.gemini_client.interactions.get(interaction.id)
|
||||
|
||||
if interaction.status == "completed":
|
||||
if self.verbose:
|
||||
self.logger.debug("Gemini research completed (async)")
|
||||
break
|
||||
elif interaction.status == "failed":
|
||||
error = getattr(interaction, 'error', 'Unknown error')
|
||||
raise RuntimeError(f"Gemini Deep Research failed: {error}")
|
||||
elif interaction.status == "cancelled":
|
||||
raise RuntimeError("Gemini Deep Research was cancelled")
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > self.max_wait_time:
|
||||
raise TimeoutError(
|
||||
f"Gemini Deep Research timed out after {self.max_wait_time}s"
|
||||
)
|
||||
|
||||
if self.verbose:
|
||||
self.logger.debug(f"Research in progress (async)... ({elapsed:.0f}s)")
|
||||
|
||||
# Use async sleep to avoid blocking event loop
|
||||
await asyncio.sleep(self.poll_interval)
|
||||
|
||||
return self._parse_gemini_response(interaction)
|
||||
|
||||
def _research_gemini_streaming(
|
||||
self,
|
||||
create_params: Dict[str, Any],
|
||||
@@ -1183,17 +1247,13 @@ You are a professional research analyst. When conducting research:
|
||||
|
||||
try:
|
||||
if self.provider == Provider.GEMINI:
|
||||
# Gemini async - run sync in executor
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: self._research_gemini(
|
||||
query=query,
|
||||
instructions=instructions,
|
||||
model=model,
|
||||
file_search=file_search if file_search is not None else self.enable_file_search,
|
||||
file_search_stores=file_search_stores or self.file_search_stores,
|
||||
)
|
||||
# Use async Gemini method for non-blocking polling
|
||||
result = await self._research_gemini_async(
|
||||
query=query,
|
||||
instructions=instructions,
|
||||
model=model,
|
||||
file_search=file_search if file_search is not None else self.enable_file_search,
|
||||
file_search_stores=file_search_stores or self.file_search_stores,
|
||||
)
|
||||
elif self.provider == Provider.LITELLM:
|
||||
# LiteLLM async
|
||||
|
||||
@@ -12,12 +12,12 @@ Follows the Agent() class patterns with:
|
||||
|
||||
import os
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Dict, Any, Union, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@dataclass
|
||||
class EmbeddingConfig:
|
||||
@@ -44,7 +44,6 @@ class EmbeddingConfig:
|
||||
"api_key": self.api_key,
|
||||
}
|
||||
|
||||
|
||||
class EmbeddingAgent:
|
||||
"""
|
||||
A specialized agent for generating text embeddings.
|
||||
@@ -164,10 +163,10 @@ class EmbeddingAgent:
|
||||
def _configure_logging(self, verbose: Union[bool, int]) -> None:
|
||||
"""Configure logging levels."""
|
||||
if isinstance(verbose, int) and verbose >= 10:
|
||||
logging.getLogger("litellm").setLevel(logging.DEBUG)
|
||||
get_logger("litellm").setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.getLogger("litellm").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
get_logger("litellm").setLevel(logging.WARNING)
|
||||
get_logger("httpx").setLevel(logging.WARNING)
|
||||
|
||||
def _get_model_params(self, model: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Build parameters for LiteLLM calls."""
|
||||
|
||||
@@ -15,6 +15,7 @@ from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
import inspect
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
import asyncio
|
||||
import threading
|
||||
import time
|
||||
@@ -22,8 +23,7 @@ import time
|
||||
if TYPE_CHECKING:
|
||||
from .agent import Agent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
class ContextPolicy(Enum):
|
||||
"""Policy for context sharing during handoff."""
|
||||
@@ -32,7 +32,6 @@ class ContextPolicy(Enum):
|
||||
NONE = "none" # No context sharing
|
||||
LAST_N = "last_n" # Share last N messages
|
||||
|
||||
|
||||
@dataclass
|
||||
class HandoffConfig:
|
||||
"""
|
||||
@@ -98,19 +97,16 @@ class HandoffConfig:
|
||||
data["context_policy"] = ContextPolicy(data["context_policy"])
|
||||
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
||||
|
||||
|
||||
class HandoffError(Exception):
|
||||
"""Base exception for handoff errors."""
|
||||
pass
|
||||
|
||||
|
||||
class HandoffCycleError(HandoffError):
|
||||
"""Raised when a cycle is detected in handoff chain."""
|
||||
def __init__(self, chain: List[str]):
|
||||
self.chain = chain
|
||||
super().__init__(f"Handoff cycle detected: {' -> '.join(chain)}")
|
||||
|
||||
|
||||
class HandoffDepthError(HandoffError):
|
||||
"""Raised when max handoff depth is exceeded."""
|
||||
def __init__(self, depth: int, max_depth: int):
|
||||
@@ -118,7 +114,6 @@ class HandoffDepthError(HandoffError):
|
||||
self.max_depth = max_depth
|
||||
super().__init__(f"Max handoff depth exceeded: {depth} > {max_depth}")
|
||||
|
||||
|
||||
class HandoffTimeoutError(HandoffError):
|
||||
"""Raised when handoff times out."""
|
||||
def __init__(self, timeout: float, agent_name: str):
|
||||
@@ -126,41 +121,34 @@ class HandoffTimeoutError(HandoffError):
|
||||
self.agent_name = agent_name
|
||||
super().__init__(f"Handoff to {agent_name} timed out after {timeout}s")
|
||||
|
||||
|
||||
# Thread-local storage for tracking handoff chains
|
||||
_handoff_context = threading.local()
|
||||
|
||||
|
||||
def _get_handoff_chain() -> List[str]:
|
||||
"""Get current handoff chain from thread-local storage."""
|
||||
if not hasattr(_handoff_context, 'chain'):
|
||||
_handoff_context.chain = []
|
||||
return _handoff_context.chain
|
||||
|
||||
|
||||
def _get_handoff_depth() -> int:
|
||||
"""Get current handoff depth."""
|
||||
return len(_get_handoff_chain())
|
||||
|
||||
|
||||
def _push_handoff(agent_name: str) -> None:
|
||||
"""Push agent to handoff chain."""
|
||||
chain = _get_handoff_chain()
|
||||
chain.append(agent_name)
|
||||
|
||||
|
||||
def _pop_handoff() -> Optional[str]:
|
||||
"""Pop agent from handoff chain."""
|
||||
chain = _get_handoff_chain()
|
||||
return chain.pop() if chain else None
|
||||
|
||||
|
||||
def _clear_handoff_chain() -> None:
|
||||
"""Clear the handoff chain."""
|
||||
if hasattr(_handoff_context, 'chain'):
|
||||
_handoff_context.chain = []
|
||||
|
||||
|
||||
@dataclass
|
||||
class HandoffInputData:
|
||||
"""Data passed to a handoff target agent."""
|
||||
@@ -170,7 +158,6 @@ class HandoffInputData:
|
||||
handoff_depth: int = 0
|
||||
handoff_chain: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class HandoffResult:
|
||||
"""Result of a handoff operation."""
|
||||
@@ -671,7 +658,6 @@ class Handoff:
|
||||
|
||||
return handoff_tool
|
||||
|
||||
|
||||
def handoff(
|
||||
agent: 'Agent',
|
||||
tool_name_override: Optional[str] = None,
|
||||
@@ -760,7 +746,6 @@ def handoff(
|
||||
config=config,
|
||||
)
|
||||
|
||||
|
||||
# Handoff filters - common patterns for filtering handoff data
|
||||
class handoff_filters:
|
||||
"""Common handoff input filters."""
|
||||
@@ -827,14 +812,12 @@ class handoff_filters:
|
||||
|
||||
return data
|
||||
|
||||
|
||||
# Recommended prompt prefix for agents that use handoffs
|
||||
RECOMMENDED_PROMPT_PREFIX = """You have the ability to transfer tasks to specialized agents when appropriate.
|
||||
When you determine that a task would be better handled by another agent with specific expertise,
|
||||
use the transfer tool to hand off the task. The receiving agent will have the full context of
|
||||
the conversation and will continue helping the user."""
|
||||
|
||||
|
||||
def prompt_with_handoff_instructions(base_prompt: str, agent: 'Agent') -> str:
|
||||
"""
|
||||
Add handoff instructions to an agent's prompt.
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
"""Heartbeat — runs an agent on a schedule, delivers results via callback.
|
||||
|
||||
Standalone wrapper class. Does NOT modify the Agent class.
|
||||
|
||||
Usage::
|
||||
|
||||
from praisonaiagents import Agent, Heartbeat
|
||||
|
||||
agent = Agent(instructions="Check server status")
|
||||
hb = Heartbeat(agent, schedule="every 30m", prompt="Report status")
|
||||
hb.start() # blocking loop
|
||||
# hb.start(blocking=False) # background thread
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Optional, Union
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@dataclass
|
||||
class HeartbeatConfig:
|
||||
"""Configuration for Heartbeat scheduler.
|
||||
|
||||
Attributes:
|
||||
schedule: Human-friendly schedule expression. Supports:
|
||||
- Keywords: "hourly", "daily", "weekly"
|
||||
- Intervals: "every 30m", "every 6h", "*/30m", "*/10s"
|
||||
- Cron: "cron:0 7 * * *"
|
||||
- One-shot: "at:2026-03-01T09:00:00"
|
||||
prompt: Override prompt sent to agent each tick. None = use agent's default.
|
||||
on_result: Callback receiving (result_text: str). Default: log to stdout.
|
||||
on_error: "retry" | "skip" | callable(error). Default: "retry".
|
||||
max_retries: Max consecutive retries before skipping. Default: 3.
|
||||
"""
|
||||
schedule: str = "hourly"
|
||||
prompt: Optional[str] = None
|
||||
on_result: Optional[Callable] = None
|
||||
on_error: Union[str, Callable] = "retry"
|
||||
max_retries: int = 3
|
||||
|
||||
class Heartbeat:
|
||||
"""Standalone heartbeat coordinator.
|
||||
|
||||
Runs an Agent on a schedule and delivers results via callback.
|
||||
Does NOT add any params to the Agent class.
|
||||
|
||||
Usage::
|
||||
|
||||
from praisonaiagents import Agent, Heartbeat
|
||||
|
||||
agent = Agent(instructions="Monitor server health")
|
||||
hb = Heartbeat(agent, schedule="hourly")
|
||||
hb.start()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
agent,
|
||||
schedule: str = "hourly",
|
||||
prompt: Optional[str] = None,
|
||||
on_result: Optional[Callable] = None,
|
||||
on_error: Union[str, Callable] = "retry",
|
||||
max_retries: int = 3,
|
||||
):
|
||||
self.agent = agent
|
||||
self.config = HeartbeatConfig(
|
||||
schedule=schedule,
|
||||
prompt=prompt,
|
||||
on_result=on_result,
|
||||
on_error=on_error,
|
||||
max_retries=max_retries,
|
||||
)
|
||||
self._running = False
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._interval_seconds = self._resolve_interval(schedule)
|
||||
|
||||
def start(self, blocking: bool = True) -> None:
|
||||
"""Start the heartbeat loop.
|
||||
|
||||
Args:
|
||||
blocking: If True, blocks the calling thread. If False,
|
||||
runs in a background daemon thread.
|
||||
"""
|
||||
self._running = True
|
||||
if blocking:
|
||||
self._loop()
|
||||
else:
|
||||
self._thread = threading.Thread(target=self._loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the heartbeat loop."""
|
||||
self._running = False
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
return self._running
|
||||
|
||||
# ── internals ────────────────────────────────────────────────────
|
||||
|
||||
def _loop(self) -> None:
|
||||
"""Main polling loop."""
|
||||
retries = 0
|
||||
while self._running:
|
||||
try:
|
||||
result = self._tick()
|
||||
retries = 0 # Reset on success
|
||||
self._deliver(result)
|
||||
except Exception as e:
|
||||
retries += 1
|
||||
logger.error(f"[heartbeat] {self.agent.name}: error on tick #{retries}: {e}")
|
||||
if callable(self.config.on_error):
|
||||
self.config.on_error(e)
|
||||
elif self.config.on_error == "skip":
|
||||
logger.warning(f"[heartbeat] Skipping error: {e}")
|
||||
else: # "retry"
|
||||
if retries >= self.config.max_retries:
|
||||
logger.error(
|
||||
f"[heartbeat] {self.agent.name}: max retries "
|
||||
f"({self.config.max_retries}) reached, skipping."
|
||||
)
|
||||
retries = 0
|
||||
time.sleep(self._interval_seconds)
|
||||
|
||||
def _tick(self) -> str:
|
||||
"""Execute one heartbeat tick — run the agent and return result."""
|
||||
prompt = self.config.prompt or "Run your scheduled check."
|
||||
result = self.agent.start(prompt)
|
||||
return str(result) if result else ""
|
||||
|
||||
def _deliver(self, result: str) -> None:
|
||||
"""Deliver result via callback or log."""
|
||||
if self.config.on_result:
|
||||
self.config.on_result(result)
|
||||
else:
|
||||
logger.info(f"[heartbeat] {self.agent.name}: {result[:200]}")
|
||||
|
||||
@staticmethod
|
||||
def _resolve_interval(schedule: str) -> float:
|
||||
"""Convert schedule string to interval in seconds.
|
||||
|
||||
Uses the scheduler parser if available, otherwise falls back to
|
||||
simple keyword parsing.
|
||||
"""
|
||||
try:
|
||||
from ..scheduler.parser import parse_schedule
|
||||
sched = parse_schedule(schedule)
|
||||
if sched.every_seconds:
|
||||
return float(sched.every_seconds)
|
||||
except (ImportError, Exception):
|
||||
pass
|
||||
|
||||
# Fallback keyword resolution
|
||||
keywords = {
|
||||
"hourly": 3600,
|
||||
"daily": 86400,
|
||||
"weekly": 604800,
|
||||
}
|
||||
lower = schedule.lower().strip()
|
||||
if lower in keywords:
|
||||
return float(keywords[lower])
|
||||
|
||||
# Try parsing "every Xm/h/s" patterns
|
||||
import re
|
||||
match = re.match(r"(?:every\s+)?(\d+)\s*(s|m|h)", lower)
|
||||
if match:
|
||||
val, unit = int(match.group(1)), match.group(2)
|
||||
multipliers = {"s": 1, "m": 60, "h": 3600}
|
||||
return float(val * multipliers.get(unit, 60))
|
||||
|
||||
return 3600.0 # Default to hourly
|
||||
@@ -7,12 +7,10 @@ from typing import Optional, Any, Dict, Union, List
|
||||
from ..agent.agent import Agent
|
||||
from pydantic import BaseModel, Field
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
import warnings
|
||||
# Filter out Pydantic warning about fields
|
||||
warnings.filterwarnings("ignore", "Valid config keys have changed in V2", UserWarning)
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn
|
||||
class ImageGenerationConfig(BaseModel):
|
||||
"""Configuration for image generation settings."""
|
||||
style: str = Field(default="natural", description="Style of the generated image")
|
||||
@@ -80,10 +78,10 @@ class ImageAgent(Agent):
|
||||
# Only suppress logs if not in debug mode
|
||||
if not isinstance(verbose, bool) and verbose >= 10:
|
||||
# Enable detailed debug logging
|
||||
logging.getLogger("asyncio").setLevel(logging.DEBUG)
|
||||
logging.getLogger("selector_events").setLevel(logging.DEBUG)
|
||||
logging.getLogger("litellm.utils").setLevel(logging.DEBUG)
|
||||
logging.getLogger("litellm.main").setLevel(logging.DEBUG)
|
||||
get_logger("asyncio").setLevel(logging.DEBUG)
|
||||
get_logger("selector_events").setLevel(logging.DEBUG)
|
||||
get_logger("litellm.utils").setLevel(logging.DEBUG)
|
||||
get_logger("litellm.main").setLevel(logging.DEBUG)
|
||||
if hasattr(self, 'litellm'):
|
||||
self.litellm.suppress_debug_messages = False
|
||||
self.litellm.set_verbose = True
|
||||
@@ -91,12 +89,12 @@ class ImageAgent(Agent):
|
||||
warnings.resetwarnings()
|
||||
else:
|
||||
# Suppress debug logging for normal operation
|
||||
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
||||
logging.getLogger("selector_events").setLevel(logging.WARNING)
|
||||
logging.getLogger("litellm.utils").setLevel(logging.WARNING)
|
||||
logging.getLogger("litellm.main").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
get_logger("asyncio").setLevel(logging.WARNING)
|
||||
get_logger("selector_events").setLevel(logging.WARNING)
|
||||
get_logger("litellm.utils").setLevel(logging.WARNING)
|
||||
get_logger("litellm.main").setLevel(logging.WARNING)
|
||||
get_logger("httpx").setLevel(logging.WARNING)
|
||||
get_logger("httpcore").setLevel(logging.WARNING)
|
||||
if hasattr(self, 'litellm'):
|
||||
self.litellm.suppress_debug_messages = True
|
||||
self.litellm._logging._disable_debugging()
|
||||
@@ -155,6 +153,7 @@ class ImageAgent(Agent):
|
||||
# Gemini provider doesn't support response_format parameter
|
||||
# Apply this filter if provider is explicitly 'gemini' or as fallback for gemini models
|
||||
config.pop('response_format', None)
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
"""
|
||||
Tool Loop Detection for PraisonAI Agents.
|
||||
|
||||
Detects when an agent is stuck calling the same tool repeatedly with
|
||||
identical arguments and no progress.
|
||||
|
||||
Inspired by OpenClaw's tool-loop-detection.ts (624 lines, 4 detectors).
|
||||
Adapted to Python with PraisonAI's protocol-driven, zero-dep philosophy.
|
||||
|
||||
Zero Performance Impact:
|
||||
- Disabled by default (enabled=False)
|
||||
- Uses stdlib only: hashlib, json
|
||||
- History is per-agent-instance (no global mutable state)
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.agent.loop_detection import (
|
||||
LoopDetectionConfig, detect_tool_loop, record_tool_call, record_tool_outcome
|
||||
)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Literal, Optional, TypedDict
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class LoopDetectionConfig:
|
||||
"""
|
||||
Configuration for tool loop detection.
|
||||
|
||||
Attributes:
|
||||
enabled: Whether loop detection is active. Default False (opt-in).
|
||||
history_size: Sliding window size for tool call history.
|
||||
warn_threshold: Number of identical (tool, args) calls before warning.
|
||||
critical_threshold: Number before critical/circuit-break. Must > warn.
|
||||
detectors: Which detectors are active.
|
||||
"""
|
||||
enabled: bool = False
|
||||
history_size: int = 30
|
||||
warn_threshold: int = 10
|
||||
critical_threshold: int = 20
|
||||
detectors: Dict[str, bool] = field(default_factory=lambda: {
|
||||
"generic_repeat": True,
|
||||
"poll_no_progress": True,
|
||||
"ping_pong": True,
|
||||
})
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Auto-correct: critical must always be > warn
|
||||
if self.critical_threshold <= self.warn_threshold:
|
||||
self.critical_threshold = self.warn_threshold + 1
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Result type
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class LoopDetectionResult(TypedDict, total=False):
|
||||
stuck: bool
|
||||
level: Optional[Literal["warning", "critical"]]
|
||||
detector: Optional[str]
|
||||
message: Optional[str]
|
||||
count: int
|
||||
|
||||
_NOT_STUCK: LoopDetectionResult = {"stuck": False}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# History record
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ToolCallRecord(TypedDict, total=False):
|
||||
tool_name: str
|
||||
args_hash: str
|
||||
result_hash: Optional[str]
|
||||
timestamp: float
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hashing helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _stable_json(value: Any) -> str:
|
||||
"""Deterministic JSON serialization (sorted keys, str fallback)."""
|
||||
if value is None or isinstance(value, (bool, int, float, str)):
|
||||
return json.dumps(value)
|
||||
if isinstance(value, (list, tuple)):
|
||||
return "[" + ",".join(_stable_json(v) for v in value) + "]"
|
||||
if isinstance(value, dict):
|
||||
keys = sorted(value.keys())
|
||||
parts = [json.dumps(k) + ":" + _stable_json(value[k]) for k in keys]
|
||||
return "{" + ",".join(parts) + "}"
|
||||
# Fallback for non-serializable objects
|
||||
try:
|
||||
return json.dumps(str(value))
|
||||
except Exception:
|
||||
return '"<unserializable>"'
|
||||
|
||||
def _sha256_hex(text: str, prefix_len: int = 16) -> str:
|
||||
return hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()[:prefix_len]
|
||||
|
||||
def hash_tool_call(tool_name: str, args: Any) -> str:
|
||||
"""
|
||||
Compute a stable hash for a (tool_name, args) pair.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool being called
|
||||
args: Arguments dict or any serializable value
|
||||
|
||||
Returns:
|
||||
16-character hex string
|
||||
"""
|
||||
stable = _stable_json({"t": tool_name, "a": args})
|
||||
return _sha256_hex(stable)
|
||||
|
||||
def _hash_result(result: Any) -> Optional[str]:
|
||||
"""Hash a tool result for no-progress detection."""
|
||||
if result is None:
|
||||
return None
|
||||
try:
|
||||
stable = _stable_json(result)
|
||||
return _sha256_hex(stable)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Record tool call / outcome
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def record_tool_call(
|
||||
history: List[ToolCallRecord],
|
||||
tool_name: str,
|
||||
args: Any,
|
||||
config: Optional[LoopDetectionConfig] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Append a tool call to the history (before execution).
|
||||
|
||||
Args:
|
||||
history: Mutable list of ToolCallRecord
|
||||
tool_name: Tool being called
|
||||
args: Tool arguments
|
||||
config: Loop detection config (for history_size)
|
||||
"""
|
||||
max_size = config.history_size if config else 30
|
||||
history.append(ToolCallRecord(
|
||||
tool_name=tool_name,
|
||||
args_hash=hash_tool_call(tool_name, args),
|
||||
result_hash=None,
|
||||
timestamp=time.time(),
|
||||
))
|
||||
# Trim to sliding window
|
||||
if len(history) > max_size:
|
||||
del history[: len(history) - max_size]
|
||||
|
||||
def record_tool_outcome(
|
||||
history: List[ToolCallRecord],
|
||||
tool_name: str,
|
||||
args: Any,
|
||||
result: Any,
|
||||
config: Optional[LoopDetectionConfig] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Update the most recent matching record with result hash (after execution).
|
||||
|
||||
Args:
|
||||
history: Mutable list of ToolCallRecord
|
||||
tool_name: Tool that was called
|
||||
args: Arguments that were passed
|
||||
result: The return value of the tool
|
||||
config: Loop detection config
|
||||
"""
|
||||
args_hash = hash_tool_call(tool_name, args)
|
||||
result_hash = _hash_result(result)
|
||||
if result_hash is None:
|
||||
return
|
||||
|
||||
# Find the latest matching record without a result_hash
|
||||
for i in range(len(history) - 1, -1, -1):
|
||||
rec = history[i]
|
||||
if rec["tool_name"] == tool_name and rec["args_hash"] == args_hash and rec.get("result_hash") is None:
|
||||
history[i] = ToolCallRecord(
|
||||
tool_name=rec["tool_name"],
|
||||
args_hash=rec["args_hash"],
|
||||
result_hash=result_hash,
|
||||
timestamp=rec["timestamp"],
|
||||
)
|
||||
return
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Detectors
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _known_poll_tool(tool_name: str) -> bool:
|
||||
"""Heuristic: tools whose names suggest polling/waiting."""
|
||||
_poll_keywords = ("status", "poll", "check", "wait", "ping", "health", "command_status")
|
||||
name_lower = tool_name.lower()
|
||||
return any(kw in name_lower for kw in _poll_keywords)
|
||||
|
||||
def _count_generic_repeat(
|
||||
history: List[ToolCallRecord],
|
||||
args_hash: str,
|
||||
tool_name: str,
|
||||
) -> int:
|
||||
"""Count how many times this exact (tool, args) appears in history."""
|
||||
return sum(
|
||||
1 for rec in history
|
||||
if rec["tool_name"] == tool_name and rec["args_hash"] == args_hash
|
||||
)
|
||||
|
||||
def _no_progress_streak(
|
||||
history: List[ToolCallRecord],
|
||||
args_hash: str,
|
||||
tool_name: str,
|
||||
) -> int:
|
||||
"""
|
||||
Count consecutive tail records with identical (args_hash, result_hash).
|
||||
Returns 0 if any record has no result_hash or a different result_hash.
|
||||
"""
|
||||
streak = 0
|
||||
first_result_hash: Optional[str] = None
|
||||
|
||||
for rec in reversed(history):
|
||||
if rec["tool_name"] != tool_name or rec["args_hash"] != args_hash:
|
||||
continue
|
||||
rh = rec.get("result_hash")
|
||||
if rh is None:
|
||||
continue
|
||||
if first_result_hash is None:
|
||||
first_result_hash = rh
|
||||
streak = 1
|
||||
elif rh == first_result_hash:
|
||||
streak += 1
|
||||
else:
|
||||
break # result changed — no longer a streak
|
||||
|
||||
return streak
|
||||
|
||||
def _ping_pong_streak(
|
||||
history: List[ToolCallRecord],
|
||||
current_hash: str,
|
||||
) -> int:
|
||||
"""
|
||||
Detect alternating pattern: A, B, A, B, ...
|
||||
Returns count of consecutive alternating tail entries, 0 if not ping-pong.
|
||||
"""
|
||||
if len(history) < 2:
|
||||
return 0
|
||||
|
||||
last = history[-1]
|
||||
# Find the "other" hash in history (most recent != last)
|
||||
other_hash: Optional[str] = None
|
||||
for rec in reversed(history[:-1]):
|
||||
if rec["args_hash"] != last["args_hash"]:
|
||||
other_hash = rec["args_hash"]
|
||||
break
|
||||
|
||||
if other_hash is None:
|
||||
return 0
|
||||
|
||||
# Count tail alternation
|
||||
count = 0
|
||||
for i, rec in enumerate(reversed(history)):
|
||||
expected = last["args_hash"] if i % 2 == 0 else other_hash
|
||||
if rec["args_hash"] != expected:
|
||||
break
|
||||
count += 1
|
||||
|
||||
# Current call must continue the ping-pong pattern
|
||||
if current_hash != (other_hash if count % 2 == 1 else last["args_hash"]):
|
||||
return 0
|
||||
|
||||
return count + 1 # include current call
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main detection function
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def detect_tool_loop(
|
||||
history: List[ToolCallRecord],
|
||||
tool_name: str,
|
||||
args: Any,
|
||||
config: Optional[LoopDetectionConfig] = None,
|
||||
) -> LoopDetectionResult:
|
||||
"""
|
||||
Detect if agent is stuck in a tool call loop.
|
||||
|
||||
Args:
|
||||
history: Mutable sliding window of ToolCallRecord
|
||||
tool_name: Tool being called now
|
||||
args: Arguments for this call
|
||||
config: Loop detection configuration
|
||||
|
||||
Returns:
|
||||
LoopDetectionResult with stuck=False (safe) or stuck=True (warning/critical)
|
||||
"""
|
||||
if config is None or not config.enabled:
|
||||
return _NOT_STUCK
|
||||
|
||||
args_hash = hash_tool_call(tool_name, args)
|
||||
detectors = config.detectors or {}
|
||||
warn = config.warn_threshold
|
||||
critical = config.critical_threshold
|
||||
|
||||
# --- poll_no_progress detector ---
|
||||
if detectors.get("poll_no_progress", True) and _known_poll_tool(tool_name):
|
||||
streak = _no_progress_streak(history, args_hash, tool_name)
|
||||
if streak >= critical:
|
||||
msg = (
|
||||
f"CRITICAL: '{tool_name}' called {streak} times with identical args "
|
||||
f"and identical result. Detected stuck polling loop. Stop and report failure."
|
||||
)
|
||||
logger.error(f"[loop_detection] {msg}")
|
||||
return LoopDetectionResult(
|
||||
stuck=True, level="critical",
|
||||
detector="poll_no_progress", message=msg, count=streak,
|
||||
)
|
||||
if streak >= warn:
|
||||
msg = (
|
||||
f"WARNING: '{tool_name}' has returned identical results {streak} times. "
|
||||
f"This looks like a stuck poll. Increase wait time or report failure."
|
||||
)
|
||||
logger.warning(f"[loop_detection] {msg}")
|
||||
return LoopDetectionResult(
|
||||
stuck=True, level="warning",
|
||||
detector="poll_no_progress", message=msg, count=streak,
|
||||
)
|
||||
|
||||
# --- ping_pong detector ---
|
||||
if detectors.get("ping_pong", True):
|
||||
pp_count = _ping_pong_streak(history, args_hash)
|
||||
if pp_count >= critical:
|
||||
msg = (
|
||||
f"CRITICAL: Ping-pong loop detected ({pp_count} alternating calls). "
|
||||
f"Agent is oscillating between two tool states. Stop and report failure."
|
||||
)
|
||||
logger.error(f"[loop_detection] {msg}")
|
||||
return LoopDetectionResult(
|
||||
stuck=True, level="critical",
|
||||
detector="ping_pong", message=msg, count=pp_count,
|
||||
)
|
||||
if pp_count >= warn:
|
||||
msg = (
|
||||
f"WARNING: Alternating tool pattern detected ({pp_count} calls). "
|
||||
f"Stop ping-pong and try a different approach."
|
||||
)
|
||||
logger.warning(f"[loop_detection] {msg}")
|
||||
return LoopDetectionResult(
|
||||
stuck=True, level="warning",
|
||||
detector="ping_pong", message=msg, count=pp_count,
|
||||
)
|
||||
|
||||
# --- generic_repeat detector ---
|
||||
if detectors.get("generic_repeat", True) and not _known_poll_tool(tool_name):
|
||||
count = _count_generic_repeat(history, args_hash, tool_name)
|
||||
if count >= critical:
|
||||
msg = (
|
||||
f"CRITICAL: '{tool_name}' called {count} times with identical args. "
|
||||
f"Agent is stuck. Execution blocked."
|
||||
)
|
||||
logger.error(f"[loop_detection] {msg}")
|
||||
return LoopDetectionResult(
|
||||
stuck=True, level="critical",
|
||||
detector="generic_repeat", message=msg, count=count,
|
||||
)
|
||||
if count >= warn:
|
||||
msg = (
|
||||
f"WARNING: '{tool_name}' called {count} times with identical args. "
|
||||
f"If not making progress, stop and report failure."
|
||||
)
|
||||
logger.warning(f"[loop_detection] {msg}")
|
||||
return LoopDetectionResult(
|
||||
stuck=True, level="warning",
|
||||
detector="generic_repeat", message=msg, count=count,
|
||||
)
|
||||
|
||||
return _NOT_STUCK
|
||||
@@ -0,0 +1,600 @@
|
||||
"""
|
||||
Memory, caching, and session persistence mixin for the Agent class.
|
||||
|
||||
Contains all methods for chat history management, caching, session
|
||||
persistence, auto-memory, and auto-learning. Extracted from agent.py
|
||||
for maintainability.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
|
||||
# Fallback helpers to avoid circular imports
|
||||
def _get_console():
|
||||
from rich.console import Console
|
||||
return Console
|
||||
|
||||
def _get_live():
|
||||
from rich.live import Live
|
||||
return Live
|
||||
|
||||
def _get_display_functions():
|
||||
from ..main import (
|
||||
display_error, display_instruction, display_interaction,
|
||||
display_generating, display_self_reflection, ReflectionOutput,
|
||||
adisplay_instruction, execute_sync_callback
|
||||
)
|
||||
return {
|
||||
'display_error': display_error,
|
||||
'display_instruction': display_instruction,
|
||||
'display_interaction': display_interaction,
|
||||
'display_generating': display_generating,
|
||||
'display_self_reflection': display_self_reflection,
|
||||
'ReflectionOutput': ReflectionOutput,
|
||||
'adisplay_instruction': adisplay_instruction,
|
||||
'execute_sync_callback': execute_sync_callback,
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
import contextlib
|
||||
from typing import List, Optional, Any, Dict, Generator, TYPE_CHECKING
|
||||
from collections import OrderedDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
class MemoryMixin:
|
||||
"""Mixin providing memory methods for the Agent class."""
|
||||
|
||||
def _cache_put(self, cache_dict, key, value):
|
||||
"""Thread-safe LRU cache put operation.
|
||||
|
||||
Args:
|
||||
cache_dict: The cache dictionary (OrderedDict)
|
||||
key: Cache key
|
||||
value: Value to cache
|
||||
"""
|
||||
with self._cache_lock:
|
||||
# Move to end if already exists (LRU update)
|
||||
if key in cache_dict:
|
||||
del cache_dict[key]
|
||||
|
||||
# Add new entry
|
||||
cache_dict[key] = value
|
||||
|
||||
# Evict oldest if over limit
|
||||
while len(cache_dict) > self._max_cache_size:
|
||||
cache_dict.popitem(last=False) # Remove oldest (FIFO)
|
||||
|
||||
def _add_to_chat_history(self, role, content):
|
||||
"""Thread-safe method to add messages to chat history.
|
||||
|
||||
Args:
|
||||
role: Message role ("user", "assistant", "system")
|
||||
content: Message content
|
||||
"""
|
||||
with self._history_lock:
|
||||
self.chat_history.append({"role": role, "content": content})
|
||||
|
||||
def _add_to_chat_history_if_not_duplicate(self, role, content):
|
||||
"""Thread-safe method to add messages to chat history only if not duplicate.
|
||||
|
||||
Atomically checks for duplicate and adds message under the same lock to prevent TOCTOU races.
|
||||
|
||||
Args:
|
||||
role: Message role ("user", "assistant", "system")
|
||||
content: Message content
|
||||
|
||||
Returns:
|
||||
bool: True if message was added, False if duplicate was detected
|
||||
"""
|
||||
with self._history_lock:
|
||||
# Check for duplicate within the same critical section
|
||||
if (self.chat_history and
|
||||
self.chat_history[-1].get("role") == role and
|
||||
self.chat_history[-1].get("content") == content):
|
||||
return False
|
||||
|
||||
# Not a duplicate, add the message
|
||||
self.chat_history.append({"role": role, "content": content})
|
||||
return True
|
||||
|
||||
def _get_chat_history_length(self):
|
||||
"""Thread-safe method to get chat history length."""
|
||||
with self._history_lock:
|
||||
return len(self.chat_history)
|
||||
|
||||
def _truncate_chat_history(self, length):
|
||||
"""Thread-safe method to truncate chat history to specified length.
|
||||
|
||||
Args:
|
||||
length: Target length for chat history
|
||||
"""
|
||||
with self._history_lock:
|
||||
self.chat_history = self.chat_history[:length]
|
||||
|
||||
def _cache_get(self, cache_dict, key):
|
||||
"""Thread-safe LRU cache get operation.
|
||||
|
||||
Args:
|
||||
cache_dict: The cache dictionary (OrderedDict)
|
||||
key: Cache key
|
||||
|
||||
Returns:
|
||||
Value if found, None otherwise
|
||||
"""
|
||||
with self._cache_lock:
|
||||
if key not in cache_dict:
|
||||
return None
|
||||
|
||||
# Move to end (mark as recently used)
|
||||
value = cache_dict[key]
|
||||
del cache_dict[key]
|
||||
cache_dict[key] = value
|
||||
return value
|
||||
|
||||
def clear_history(self):
|
||||
"""Clear all chat history.
|
||||
|
||||
Also resets _auto_save_last_index to prevent silent message loss
|
||||
when auto_save is enabled.
|
||||
"""
|
||||
self.chat_history = []
|
||||
# Reset auto-save index to prevent stale index causing message loss
|
||||
self._auto_save_last_index = 0
|
||||
|
||||
def prune_history(self, keep_last: int = 5) -> int:
|
||||
"""
|
||||
Prune chat history to keep only the last N messages.
|
||||
|
||||
Useful for cleaning up large history after image analysis sessions
|
||||
to prevent context window saturation.
|
||||
|
||||
Args:
|
||||
keep_last: Number of recent messages to keep
|
||||
|
||||
Returns:
|
||||
Number of messages deleted
|
||||
"""
|
||||
with self._history_lock:
|
||||
if len(self.chat_history) <= keep_last:
|
||||
return 0
|
||||
|
||||
deleted_count = len(self.chat_history) - keep_last
|
||||
self.chat_history = self.chat_history[-keep_last:]
|
||||
# Reset auto-save index to match new history length
|
||||
self._auto_save_last_index = len(self.chat_history)
|
||||
return deleted_count
|
||||
|
||||
def delete_history(self, index: int) -> bool:
|
||||
"""
|
||||
Delete a specific message from chat history by index.
|
||||
|
||||
Supports negative indexing (-1 for last message, etc.).
|
||||
|
||||
Args:
|
||||
index: Message index (0-based, supports negative indexing)
|
||||
|
||||
Returns:
|
||||
True if deleted, False if index out of range
|
||||
"""
|
||||
with self._history_lock:
|
||||
try:
|
||||
del self.chat_history[index]
|
||||
# Adjust auto-save index if deletion affects saved range
|
||||
if hasattr(self, '_auto_save_last_index'):
|
||||
self._auto_save_last_index = min(
|
||||
self._auto_save_last_index,
|
||||
len(self.chat_history)
|
||||
)
|
||||
return True
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
def delete_history_matching(self, pattern: str) -> int:
|
||||
"""
|
||||
Delete all messages matching a pattern.
|
||||
|
||||
Useful for removing all image-related messages after processing.
|
||||
|
||||
Args:
|
||||
pattern: Substring to match in message content
|
||||
|
||||
Returns:
|
||||
Number of messages deleted
|
||||
"""
|
||||
with self._history_lock:
|
||||
original_len = len(self.chat_history)
|
||||
self.chat_history = [
|
||||
msg for msg in self.chat_history
|
||||
if pattern.lower() not in msg.get("content", "").lower()
|
||||
]
|
||||
deleted_count = original_len - len(self.chat_history)
|
||||
# Adjust auto-save index if deletion affects saved range
|
||||
if deleted_count > 0 and hasattr(self, '_auto_save_last_index'):
|
||||
self._auto_save_last_index = min(
|
||||
self._auto_save_last_index,
|
||||
len(self.chat_history)
|
||||
)
|
||||
return deleted_count
|
||||
|
||||
def get_history_size(self) -> int:
|
||||
"""Get the current number of messages in chat history."""
|
||||
return len(self.chat_history)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def ephemeral(self) -> Generator[None, None, None]:
|
||||
"""
|
||||
Context manager for ephemeral conversations.
|
||||
|
||||
Messages within this block are NOT permanently stored in chat_history.
|
||||
History is restored to pre-block state after exiting.
|
||||
|
||||
Example:
|
||||
with agent.ephemeral():
|
||||
response = agent.chat("[IMAGE] Analyze this")
|
||||
# After block, history is restored - image NOT persisted
|
||||
"""
|
||||
# Save current history state
|
||||
with self._history_lock:
|
||||
saved_history = self.chat_history.copy()
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
# Restore history to pre-block state
|
||||
with self._history_lock:
|
||||
self.chat_history = saved_history
|
||||
|
||||
def _init_db_session(self):
|
||||
"""Initialize DB session if db adapter is provided (lazy, first chat only)."""
|
||||
if self._db is None or self._db_initialized:
|
||||
return
|
||||
|
||||
# Generate session_id if not provided: default to per-hour ID (YYYYMMDDHH-agentname)
|
||||
if self._session_id is None:
|
||||
import hashlib
|
||||
from datetime import datetime, timezone
|
||||
# Per-hour session ID: YYYYMMDDHH (UTC) + agent name hash for uniqueness
|
||||
hour_str = datetime.now(timezone.utc).strftime("%Y%m%d%H")
|
||||
agent_hash = hashlib.md5((self.name or "agent").encode()).hexdigest()[:6]
|
||||
self._session_id = f"{hour_str}-{agent_hash}"
|
||||
|
||||
# Call db adapter's on_agent_start to get previous messages
|
||||
try:
|
||||
history = self._db.on_agent_start(
|
||||
agent_name=self.name,
|
||||
session_id=self._session_id,
|
||||
user_id=self.user_id,
|
||||
metadata={"role": self.role, "goal": self.goal}
|
||||
)
|
||||
|
||||
# Restore chat history from previous session
|
||||
if history:
|
||||
for msg in history:
|
||||
self.chat_history.append({
|
||||
"role": msg.role,
|
||||
"content": msg.content
|
||||
})
|
||||
logging.info(f"Resumed session {self._session_id} with {len(history)} messages")
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to initialize DB session: {e}")
|
||||
|
||||
self._db_initialized = True
|
||||
self._current_run_id = None # Track current run
|
||||
|
||||
def _init_session_store(self):
|
||||
"""
|
||||
Initialize session store for JSON-based persistence (lazy, first chat only).
|
||||
|
||||
This is used when session_id is provided but no DB adapter.
|
||||
Enables automatic session persistence with zero configuration.
|
||||
"""
|
||||
if self._session_store_initialized:
|
||||
return
|
||||
|
||||
# Only initialize if session_id is provided and no DB adapter
|
||||
if self._session_id is None or self._db is not None:
|
||||
self._session_store_initialized = True
|
||||
return
|
||||
|
||||
try:
|
||||
from ..session import get_default_session_store
|
||||
self._session_store = get_default_session_store()
|
||||
|
||||
# Restore chat history from previous session
|
||||
history = self._session_store.get_chat_history(self._session_id)
|
||||
if history:
|
||||
# Only restore if chat_history is empty (avoid duplicates)
|
||||
if not self.chat_history:
|
||||
for msg in history:
|
||||
self.chat_history.append({
|
||||
"role": msg["role"],
|
||||
"content": msg["content"]
|
||||
})
|
||||
logging.info(f"Restored session {self._session_id} with {len(history)} messages from JSON store")
|
||||
|
||||
# Set agent info
|
||||
self._session_store.set_agent_info(
|
||||
self._session_id,
|
||||
agent_name=self.name,
|
||||
user_id=self.user_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to initialize session store: {e}")
|
||||
self._session_store = None
|
||||
|
||||
self._session_store_initialized = True
|
||||
|
||||
def _start_run(self, input_content: str):
|
||||
"""Start a new run (turn) for persistence tracking."""
|
||||
if self._db is None:
|
||||
return
|
||||
|
||||
import uuid
|
||||
self._current_run_id = f"run-{uuid.uuid4().hex[:12]}"
|
||||
|
||||
try:
|
||||
if hasattr(self._db, 'on_run_start'):
|
||||
self._db.on_run_start(
|
||||
session_id=self._session_id,
|
||||
run_id=self._current_run_id,
|
||||
input_content=input_content,
|
||||
metadata={"agent_name": self.name}
|
||||
)
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to start run: {e}")
|
||||
|
||||
def _end_run(self, output_content: str, status: str = "completed", metrics: dict = None):
|
||||
"""End the current run (turn)."""
|
||||
if self._db is None or self._current_run_id is None:
|
||||
return
|
||||
|
||||
try:
|
||||
if hasattr(self._db, 'on_run_end'):
|
||||
self._db.on_run_end(
|
||||
session_id=self._session_id,
|
||||
run_id=self._current_run_id,
|
||||
output_content=output_content,
|
||||
status=status,
|
||||
metrics=metrics or {},
|
||||
metadata={"agent_name": self.name}
|
||||
)
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to end run: {e}")
|
||||
|
||||
self._current_run_id = None
|
||||
|
||||
def _persist_message(self, role: str, content: str):
|
||||
"""Persist a message to the DB or session store."""
|
||||
# Try DB adapter first
|
||||
if self._db is not None:
|
||||
try:
|
||||
if role == "user":
|
||||
self._db.on_user_message(self._session_id, content)
|
||||
elif role == "assistant":
|
||||
self._db.on_agent_message(self._session_id, content)
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to persist message to DB: {e}")
|
||||
return
|
||||
|
||||
# Fall back to session store (JSON-based)
|
||||
if self._session_store is not None and self._session_id is not None:
|
||||
try:
|
||||
if role == "user":
|
||||
self._session_store.add_user_message(self._session_id, content)
|
||||
elif role == "assistant":
|
||||
self._session_store.add_assistant_message(self._session_id, content)
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to persist message to session store: {e}")
|
||||
|
||||
def session_id(self) -> Optional[str]:
|
||||
"""Get the current session ID."""
|
||||
return self._session_id
|
||||
|
||||
def _load_history_context(self):
|
||||
"""Load history from past sessions into context.
|
||||
|
||||
Note: This functionality is now handled via context= param with ManagerConfig.
|
||||
This method is kept for backward compatibility but is a no-op.
|
||||
Use context=ManagerConfig(history_sessions=N) to load past sessions.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _process_auto_memory(self, user_message: str, assistant_response: str):
|
||||
"""Process auto-memory extraction after agent response.
|
||||
|
||||
Called after each agent response when auto_memory=True in MemoryConfig.
|
||||
Uses AutoMemory to extract and store memorable content (names, preferences,
|
||||
facts) from the conversation. No-op when auto_memory is disabled.
|
||||
|
||||
Args:
|
||||
user_message: The user's input message
|
||||
assistant_response: The agent's response
|
||||
"""
|
||||
if not self._auto_memory or not self._memory_instance:
|
||||
return
|
||||
|
||||
try:
|
||||
from ..memory.auto_memory import AutoMemory
|
||||
# Lazy-create AutoMemory wrapper on first use
|
||||
if not hasattr(self, '_auto_memory_instance') or self._auto_memory_instance is None:
|
||||
self._auto_memory_instance = AutoMemory(
|
||||
self._memory_instance,
|
||||
enabled=True,
|
||||
verbose=1 if getattr(self, 'verbose', False) else 0
|
||||
)
|
||||
self._auto_memory_instance.process_interaction(
|
||||
user_message=str(user_message),
|
||||
assistant_response=str(assistant_response),
|
||||
)
|
||||
except Exception as e:
|
||||
logging.debug(f"Auto-memory extraction failed: {e}")
|
||||
|
||||
def _process_auto_learning(self):
|
||||
"""Process auto-learning extraction after agent response.
|
||||
|
||||
Called after each agent response when LearnMode.AGENTIC is set.
|
||||
Uses LearnManager.process_conversation() to extract and store learnings
|
||||
(persona, insights, patterns) from the conversation. No-op when learning
|
||||
is disabled or mode is not AGENTIC.
|
||||
"""
|
||||
# Quick exit if no learn config or mode is not AGENTIC
|
||||
if not self._learn_config:
|
||||
return
|
||||
|
||||
# Check if mode is AGENTIC (auto-extract learnings)
|
||||
from ..memory.learn.protocols import LearnMode
|
||||
mode = getattr(self._learn_config, 'mode', None)
|
||||
|
||||
# Handle PROPOSE mode — extract but store as pending for user approval
|
||||
if (isinstance(mode, LearnMode) and mode == LearnMode.PROPOSE) or \
|
||||
(isinstance(mode, str) and mode == 'propose'):
|
||||
learn_manager = None
|
||||
if self._memory_instance and hasattr(self._memory_instance, 'learn'):
|
||||
learn_manager = self._memory_instance.learn
|
||||
if learn_manager:
|
||||
try:
|
||||
recent_messages = self.chat_history[-2:] if len(self.chat_history) >= 2 else self.chat_history
|
||||
if recent_messages:
|
||||
# Use same extraction as AGENTIC, but don't auto-store
|
||||
result = learn_manager.process_conversation(
|
||||
messages=recent_messages,
|
||||
llm=getattr(self._learn_config, 'llm', None) or self.llm,
|
||||
extract_only=True,
|
||||
)
|
||||
# Move extracted items to pending queue
|
||||
if result:
|
||||
for p in result.get("persona", []):
|
||||
if p:
|
||||
learn_manager.add_pending(p, category="persona")
|
||||
for i in result.get("insights", []):
|
||||
if i:
|
||||
learn_manager.add_pending(i, category="insights")
|
||||
for pt in result.get("patterns", []):
|
||||
if pt:
|
||||
learn_manager.add_pending(pt, category="patterns")
|
||||
except Exception as e:
|
||||
logging.debug(f"PROPOSE mode learning extraction failed: {e}")
|
||||
return
|
||||
|
||||
if mode is None or (isinstance(mode, LearnMode) and mode != LearnMode.AGENTIC):
|
||||
return
|
||||
if isinstance(mode, str) and mode != 'agentic':
|
||||
return
|
||||
|
||||
# Get LearnManager from memory instance
|
||||
learn_manager = None
|
||||
if self._memory_instance and hasattr(self._memory_instance, 'learn'):
|
||||
learn_manager = self._memory_instance.learn
|
||||
|
||||
if not learn_manager:
|
||||
return
|
||||
|
||||
try:
|
||||
# Process recent conversation (last 2 messages: user + assistant)
|
||||
recent_messages = self.chat_history[-2:] if len(self.chat_history) >= 2 else self.chat_history
|
||||
if recent_messages:
|
||||
learn_manager.process_conversation(
|
||||
messages=recent_messages,
|
||||
llm=getattr(self._learn_config, 'llm', None) or self.llm,
|
||||
)
|
||||
except Exception as e:
|
||||
logging.debug(f"Auto-learning extraction failed: {e}")
|
||||
|
||||
def _auto_save_session(self):
|
||||
"""Auto-save session if auto_save is enabled.
|
||||
|
||||
G-1 FIX: Routes to SessionStore instead of Memory.save_session() to
|
||||
maintain clean separation between conversation history (SessionStore)
|
||||
and semantic memory (Memory).
|
||||
|
||||
Issue 1 FIX: Track last saved index to avoid duplicate insertion when
|
||||
called multiple times in the same session.
|
||||
"""
|
||||
if not self.auto_save:
|
||||
return
|
||||
|
||||
try:
|
||||
# Filter out history markers before saving
|
||||
clean_history = [
|
||||
{k: v for k, v in msg.items() if k != "_from_history"}
|
||||
for msg in self.chat_history
|
||||
]
|
||||
|
||||
# Issue 1 FIX: Only save NEW messages since last save
|
||||
# Track last saved index to avoid duplicates
|
||||
last_saved = getattr(self, '_auto_save_last_index', 0)
|
||||
new_messages = clean_history[last_saved:]
|
||||
|
||||
if not new_messages:
|
||||
return # Nothing new to save
|
||||
|
||||
# G-1 FIX: Use SessionStore for conversation history persistence
|
||||
# This maintains clean separation: Memory = facts, SessionStore = turns
|
||||
if self._session_store is not None:
|
||||
# Persist only NEW messages to SessionStore
|
||||
for msg in new_messages:
|
||||
self._session_store.add_message(
|
||||
self.auto_save, # Use auto_save name as session_id
|
||||
role=msg.get("role", "user"),
|
||||
content=msg.get("content", ""),
|
||||
)
|
||||
# Update last saved index
|
||||
self._auto_save_last_index = len(clean_history)
|
||||
logging.debug(f"Auto-saved {len(new_messages)} new messages to SessionStore: {self.auto_save}")
|
||||
elif self._memory_instance and hasattr(self._memory_instance, 'save_session'):
|
||||
# Fallback to Memory.save_session() for backward compatibility
|
||||
# Memory.save_session() replaces entire history, so no duplicate issue
|
||||
self._memory_instance.save_session(
|
||||
name=self.auto_save,
|
||||
conversation_history=clean_history,
|
||||
metadata={"agent_name": self.name, "user_id": self.user_id}
|
||||
)
|
||||
logging.debug(f"Auto-saved session to Memory: {self.auto_save}")
|
||||
except Exception as e:
|
||||
logging.debug(f"Error auto-saving session: {e}")
|
||||
|
||||
def _save_output_to_file(self, content: str) -> bool:
|
||||
"""Save agent output to file if output_file is configured.
|
||||
|
||||
Args:
|
||||
content: The response content to save
|
||||
|
||||
Returns:
|
||||
True if file was saved, False otherwise
|
||||
"""
|
||||
if not self._output_file:
|
||||
return False
|
||||
|
||||
try:
|
||||
import os
|
||||
|
||||
# Expand user home directory and resolve path
|
||||
file_path = os.path.expanduser(self._output_file)
|
||||
file_path = os.path.abspath(file_path)
|
||||
|
||||
# Create parent directories if they don't exist
|
||||
parent_dir = os.path.dirname(file_path)
|
||||
if parent_dir and not os.path.exists(parent_dir):
|
||||
os.makedirs(parent_dir, exist_ok=True)
|
||||
|
||||
# Write content to file
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(str(content))
|
||||
|
||||
# Print success message to terminal
|
||||
print(f"✅ Output saved to {file_path}")
|
||||
logging.debug(f"Output saved to file: {file_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to save output to file '{self._output_file}': {e}")
|
||||
print(f"⚠️ Failed to save output to {self._output_file}: {e}")
|
||||
return False
|
||||
|
||||
@@ -9,12 +9,12 @@ import threading
|
||||
import heapq
|
||||
import time
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
from typing import Optional, Any, List, Tuple
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
class MessagePriority(Enum):
|
||||
"""Message priority levels."""
|
||||
@@ -23,7 +23,6 @@ class MessagePriority(Enum):
|
||||
HIGH = 10
|
||||
URGENT = 20
|
||||
|
||||
|
||||
@dataclass(order=True)
|
||||
class QueuedMessage:
|
||||
"""A message in the queue with priority and timestamp."""
|
||||
@@ -36,7 +35,6 @@ class QueuedMessage:
|
||||
# Negate priority for max-heap behavior (higher priority = lower number in heap)
|
||||
self.priority = -self.priority
|
||||
|
||||
|
||||
class AgentMessageQueue:
|
||||
"""
|
||||
Thread-safe message queue for agent prompts.
|
||||
@@ -158,7 +156,6 @@ class AgentMessageQueue:
|
||||
"""Total number of messages ever enqueued."""
|
||||
return self._message_count
|
||||
|
||||
|
||||
class AsyncAgentMessageQueue:
|
||||
"""
|
||||
Async-compatible message queue for agent prompts.
|
||||
|
||||
@@ -9,6 +9,7 @@ Follows the Agent() class patterns:
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
import warnings
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Any, Dict, Union, List
|
||||
@@ -16,7 +17,6 @@ from pathlib import Path
|
||||
|
||||
warnings.filterwarnings("ignore", "Valid config keys have changed in V2", UserWarning)
|
||||
|
||||
|
||||
@dataclass
|
||||
class OCRConfig:
|
||||
"""Configuration for OCR settings."""
|
||||
@@ -38,7 +38,6 @@ class OCRConfig:
|
||||
"api_key": self.api_key,
|
||||
}
|
||||
|
||||
|
||||
class OCRAgent:
|
||||
"""
|
||||
A specialized agent for OCR (Optical Character Recognition).
|
||||
@@ -143,10 +142,10 @@ class OCRAgent:
|
||||
def _configure_logging(self, verbose: Union[bool, int]) -> None:
|
||||
"""Configure logging levels."""
|
||||
if isinstance(verbose, int) and verbose >= 10:
|
||||
logging.getLogger("litellm").setLevel(logging.DEBUG)
|
||||
get_logger("litellm").setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.getLogger("litellm").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
get_logger("litellm").setLevel(logging.WARNING)
|
||||
get_logger("httpx").setLevel(logging.WARNING)
|
||||
|
||||
def _get_model_params(self, model: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Build parameters for LiteLLM calls."""
|
||||
|
||||
@@ -34,6 +34,7 @@ Example:
|
||||
"""
|
||||
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
from typing import List, Optional, Any, Dict
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
@@ -47,7 +48,6 @@ class ExpandStrategy(Enum):
|
||||
CREATIVE = "creative"
|
||||
AUTO = "auto"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExpandResult:
|
||||
"""Result of a prompt expansion operation."""
|
||||
@@ -59,7 +59,6 @@ class ExpandResult:
|
||||
def __repr__(self):
|
||||
return f"ExpandResult(strategy={self.strategy_used.value}, original_len={len(self.original_prompt)}, expanded_len={len(self.expanded_prompt)})"
|
||||
|
||||
|
||||
class PromptExpanderAgent:
|
||||
"""
|
||||
Agent for expanding short prompts into detailed, comprehensive prompts.
|
||||
@@ -213,7 +212,7 @@ Expanded prompt:"""
|
||||
self._agent = None # Lazy initialized
|
||||
|
||||
# Setup logging
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger = get_logger(__name__)
|
||||
if self.verbose:
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@@ -228,6 +228,80 @@ class ContextEngineerProtocol(Protocol):
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class HttpLauncherProtocol(Protocol):
|
||||
"""
|
||||
Protocol for HTTP server launchers.
|
||||
|
||||
This defines the interface for launching HTTP servers for agents.
|
||||
The core SDK defines the protocol, while heavy implementations
|
||||
(FastAPI, uvicorn) live in the wrapper layer.
|
||||
|
||||
Example:
|
||||
```python
|
||||
class FastAPILauncher:
|
||||
def launch(self, agent, path="/", port=8000, host="0.0.0.0", debug=False):
|
||||
# FastAPI implementation
|
||||
pass
|
||||
|
||||
# In Agent class
|
||||
launcher: HttpLauncherProtocol = get_http_launcher()
|
||||
launcher.launch(self, path, port, host, debug)
|
||||
```
|
||||
"""
|
||||
|
||||
def launch(
|
||||
self,
|
||||
agent: Any,
|
||||
path: str = "/",
|
||||
port: int = 8000,
|
||||
host: str = "0.0.0.0",
|
||||
debug: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Launch HTTP server for the agent.
|
||||
|
||||
Args:
|
||||
agent: The agent instance to serve
|
||||
path: API endpoint path (default: '/')
|
||||
port: Server port (default: 8000)
|
||||
host: Server host (default: '0.0.0.0')
|
||||
debug: Enable debug mode (default: False)
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class McpLauncherProtocol(Protocol):
|
||||
"""
|
||||
Protocol for MCP (Model Context Protocol) server launchers.
|
||||
|
||||
This defines the interface for launching MCP servers for agents.
|
||||
The core SDK defines the protocol, while heavy implementations
|
||||
(FastMCP, uvicorn) live in the wrapper layer.
|
||||
"""
|
||||
|
||||
def launch(
|
||||
self,
|
||||
agent: Any,
|
||||
path: str = "/",
|
||||
port: int = 8000,
|
||||
host: str = "0.0.0.0",
|
||||
debug: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Launch MCP server for the agent.
|
||||
|
||||
Args:
|
||||
agent: The agent instance to serve
|
||||
path: Base path for MCP endpoints
|
||||
port: Server port (default: 8000)
|
||||
host: Server host (default: '0.0.0.0')
|
||||
debug: Enable debug mode (default: False)
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AgentProtocol',
|
||||
'RunnableAgentProtocol',
|
||||
@@ -235,4 +309,6 @@ __all__ = [
|
||||
'MemoryAwareAgentProtocol',
|
||||
'FullAgentProtocol',
|
||||
'ContextEngineerProtocol',
|
||||
'HttpLauncherProtocol',
|
||||
'McpLauncherProtocol',
|
||||
]
|
||||
@@ -39,12 +39,12 @@ Example:
|
||||
"""
|
||||
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
import json
|
||||
from typing import List, Optional, Any, Dict
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class RewriteStrategy(Enum):
|
||||
"""Enumeration of available query rewriting strategies."""
|
||||
BASIC = "basic"
|
||||
@@ -55,7 +55,6 @@ class RewriteStrategy(Enum):
|
||||
CONTEXTUAL = "contextual"
|
||||
AUTO = "auto"
|
||||
|
||||
|
||||
@dataclass
|
||||
class RewriteResult:
|
||||
"""Result of a query rewriting operation."""
|
||||
@@ -86,7 +85,6 @@ class RewriteResult:
|
||||
queries.extend(self.sub_queries)
|
||||
return list(set(queries)) # Remove duplicates
|
||||
|
||||
|
||||
class QueryRewriterAgent:
|
||||
"""
|
||||
Agent for rewriting queries to improve retrieval quality in RAG applications.
|
||||
@@ -271,7 +269,7 @@ Improved version:"""
|
||||
}
|
||||
|
||||
# Setup logging
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger = get_logger(__name__)
|
||||
if self.verbose:
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from dataclasses import dataclass, field, asdict
|
||||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from praisonaiagents._logging import get_logger
|
||||
|
||||
@dataclass
|
||||
class RealtimeConfig:
|
||||
@@ -33,7 +33,6 @@ class RealtimeConfig:
|
||||
"""Convert config to dictionary."""
|
||||
return {k: v for k, v in asdict(self).items() if v is not None}
|
||||
|
||||
|
||||
class RealtimeAgent:
|
||||
"""Agent for real-time voice conversations using OpenAI Realtime API.
|
||||
|
||||
@@ -107,7 +106,7 @@ class RealtimeAgent:
|
||||
self._connected = False
|
||||
|
||||
# Configure logging
|
||||
self._logger = logging.getLogger(f"RealtimeAgent.{name}")
|
||||
self._logger = get_logger(f"RealtimeAgent.{name}")
|
||||
if not verbose:
|
||||
self._logger.setLevel(logging.WARNING)
|
||||
|
||||
@@ -138,16 +137,20 @@ class RealtimeAgent:
|
||||
True if connected successfully
|
||||
"""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
# If already in async context, can't use run_until_complete
|
||||
self._log("Use aconnect() in async context", "yellow")
|
||||
return False
|
||||
return loop.run_until_complete(self.aconnect(**kwargs))
|
||||
# Try to get the running loop - if this succeeds, we're in async context
|
||||
loop = asyncio.get_running_loop()
|
||||
# If already in async context, can't use run_until_complete
|
||||
self._log("Use aconnect() in async context", "yellow")
|
||||
return False
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop.run_until_complete(self.aconnect(**kwargs))
|
||||
# No running loop, safe to create/get one and run
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
return loop.run_until_complete(self.aconnect(**kwargs))
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop.run_until_complete(self.aconnect(**kwargs))
|
||||
|
||||
async def aconnect(self, **kwargs) -> bool:
|
||||
"""Connect to the Realtime API asynchronously.
|
||||
@@ -209,15 +212,19 @@ class RealtimeAgent:
|
||||
True if disconnected successfully
|
||||
"""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
self._log("Use adisconnect() in async context", "yellow")
|
||||
return False
|
||||
return loop.run_until_complete(self.adisconnect())
|
||||
# Try to get the running loop - if this succeeds, we're in async context
|
||||
loop = asyncio.get_running_loop()
|
||||
self._log("Use adisconnect() in async context", "yellow")
|
||||
return False
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop.run_until_complete(self.adisconnect())
|
||||
# No running loop, safe to create/get one and run
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
return loop.run_until_complete(self.adisconnect())
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop.run_until_complete(self.adisconnect())
|
||||
|
||||
async def adisconnect(self) -> bool:
|
||||
"""Disconnect from the Realtime API asynchronously.
|
||||
@@ -273,15 +280,19 @@ class RealtimeAgent:
|
||||
True if sent successfully
|
||||
"""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
self._log("Use async send in async context", "yellow")
|
||||
return False
|
||||
return loop.run_until_complete(self.asend_text(text))
|
||||
# Try to get the running loop - if this succeeds, we're in async context
|
||||
loop = asyncio.get_running_loop()
|
||||
self._log("Use async send in async context", "yellow")
|
||||
return False
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop.run_until_complete(self.asend_text(text))
|
||||
# No running loop, safe to create/get one and run
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
return loop.run_until_complete(self.asend_text(text))
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop.run_until_complete(self.asend_text(text))
|
||||
|
||||
async def asend_text(self, text: str) -> bool:
|
||||
"""Send text message asynchronously.
|
||||
@@ -315,14 +326,18 @@ class RealtimeAgent:
|
||||
True if sent successfully
|
||||
"""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
return False
|
||||
return loop.run_until_complete(self.asend_audio(audio_data))
|
||||
# Try to get the running loop - if this succeeds, we're in async context
|
||||
loop = asyncio.get_running_loop()
|
||||
return False
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop.run_until_complete(self.asend_audio(audio_data))
|
||||
# No running loop, safe to create/get one and run
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
return loop.run_until_complete(self.asend_audio(audio_data))
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop.run_until_complete(self.asend_audio(audio_data))
|
||||
|
||||
async def asend_audio(self, audio_data: bytes) -> bool:
|
||||
"""Send audio data asynchronously.
|
||||
|
||||
@@ -7,13 +7,14 @@ model selection based on task characteristics.
|
||||
|
||||
import os
|
||||
import logging
|
||||
from praisonaiagents._logging import get_logger
|
||||
from typing import Dict, List, Optional, Any, Union
|
||||
from .agent import Agent
|
||||
from ..llm.model_router import ModelRouter
|
||||
from ..llm import LLM
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from ..llm import LLM, TokenUsage
|
||||
from ..trace.protocol import get_default_emitter
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
class RouterAgent(Agent):
|
||||
"""
|
||||
@@ -213,8 +214,8 @@ class RouterAgent(Agent):
|
||||
full_prompt = f"{context}\n\n{prompt}"
|
||||
|
||||
try:
|
||||
# Execute with the selected model
|
||||
response = llm_instance.get_response(
|
||||
# Execute with the selected model, requesting token usage tracking
|
||||
result = llm_instance.get_response(
|
||||
prompt=full_prompt,
|
||||
system_prompt=self._build_system_prompt(),
|
||||
tools=tools,
|
||||
@@ -225,16 +226,45 @@ class RouterAgent(Agent):
|
||||
agent_role=self.role,
|
||||
agent_tools=[t.__name__ if hasattr(t, '__name__') else str(t) for t in (tools or [])],
|
||||
execute_tool_fn=self.execute_tool if tools else None,
|
||||
return_token_usage=True, # Request token usage information
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# Extract response and token usage
|
||||
if isinstance(result, tuple):
|
||||
response, token_usage = result
|
||||
else:
|
||||
# Fallback for backward compatibility
|
||||
response = result
|
||||
token_usage = TokenUsage()
|
||||
|
||||
# Update usage statistics
|
||||
self.model_usage_stats[model_name]['calls'] += 1
|
||||
self.model_usage_stats[model_name]['tokens'] += token_usage.total_tokens
|
||||
|
||||
# TODO: Implement token tracking when LLM.get_response() is updated to return token usage
|
||||
# The LLM response currently returns only text, but litellm provides usage info in:
|
||||
# response.get("usage") with prompt_tokens, completion_tokens, and total_tokens
|
||||
# This would require modifying the LLM class to return both text and metadata
|
||||
# Calculate and store cost estimate
|
||||
model_info = self.model_router.get_model_info(model_name)
|
||||
if model_info and token_usage.total_tokens > 0:
|
||||
cost = self.model_router.estimate_cost(model_name, token_usage.total_tokens)
|
||||
self.model_usage_stats[model_name]['cost'] += cost
|
||||
|
||||
# Emit token usage via trace system for observability
|
||||
try:
|
||||
trace_emitter = get_default_emitter()
|
||||
trace_emitter.output(
|
||||
content=f"RouterAgent routing decision completed",
|
||||
agent_name=self.name,
|
||||
metadata={
|
||||
'selected_model': model_name,
|
||||
'routing_strategy': self.routing_strategy,
|
||||
'token_usage': token_usage.to_dict(),
|
||||
'estimated_cost': self.model_usage_stats[model_name]['cost'],
|
||||
'total_calls': self.model_usage_stats[model_name]['calls'],
|
||||
}
|
||||
)
|
||||
except Exception as trace_error:
|
||||
# Don't fail the request if tracing fails
|
||||
logger.debug(f"Failed to emit trace event: {trace_error}")
|
||||
|
||||
return response
|
||||
|
||||
|
||||