Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3414e55b1b | ||
|
|
6e363e470a | ||
|
|
dd0e02c056 | ||
|
|
365f75040f | ||
|
|
88f364c34d | ||
|
|
4ac0d883cf | ||
|
|
78af42457c | ||
|
|
0accebb2e3 | ||
|
|
757559f67f | ||
|
|
43a5d097dd | ||
|
|
18b4914de2 | ||
|
|
4675577a3f | ||
|
|
426ecfc90a | ||
|
|
c9b8685d27 | ||
|
|
7ea4b9ad13 | ||
|
|
defa325754 | ||
|
|
64d048c897 | ||
|
|
2806f47093 | ||
|
|
8db093245e | ||
|
|
78523f8911 | ||
|
|
51b8761303 | ||
|
|
c8144d6532 | ||
|
|
9327462a89 | ||
|
|
24f1945c70 | ||
|
|
9ac9dc5aed | ||
|
|
fde17acdc8 | ||
|
|
5ed5f1a6a9 | ||
|
|
cc1853f0dc | ||
|
|
147aa701dd | ||
|
|
6496455c45 | ||
|
|
239660715a | ||
|
|
8907022b3a | ||
|
|
bb0d1968ad | ||
|
|
2e7ffe7338 | ||
|
|
a2b44dc100 | ||
|
|
ca4ee2e42b | ||
|
|
40f8d0d62c | ||
|
|
f74012e374 | ||
|
|
1145252406 | ||
|
|
f74ee5d619 | ||
|
|
fe30293e85 | ||
|
|
6865574255 | ||
|
|
b4e3a8a84a | ||
|
|
0889f800c1 | ||
|
|
e84d2b18da | ||
|
|
dfbc68f92f | ||
|
|
4b806f8eca | ||
|
|
f00763937b | ||
|
|
d0523d5b64 | ||
|
|
dd41a3c426 | ||
|
|
e87cc650dc | ||
|
|
3ac10ab851 | ||
|
|
2ecc383e6e | ||
|
|
35761ea88e | ||
|
|
e122d7d227 | ||
|
|
2284535f65 | ||
|
|
eb2cb40b30 | ||
|
|
459688d32d | ||
|
|
b7f3e55669 | ||
|
|
1cd48fde4e | ||
|
|
584d85f305 | ||
|
|
42807f1740 | ||
|
|
0d40fa93aa | ||
|
|
75439026e7 | ||
|
|
616532d4ed | ||
|
|
6693274671 | ||
|
|
142db6c5d5 | ||
|
|
a2d915f6ab | ||
|
|
e40269d194 | ||
|
|
d2ce315ce1 | ||
|
|
f069930d67 | ||
|
|
b58a85a669 | ||
|
|
6d60068661 | ||
|
|
5fa2a7fc47 | ||
|
|
fbdcb92ff2 | ||
|
|
18da39991a | ||
|
|
1c32d811f7 | ||
|
|
27de2635eb | ||
|
|
d6bdc7e0de | ||
|
|
92c6a13cac | ||
|
|
5882eb0ffc | ||
|
|
7bcba30b14 | ||
|
|
ce3c3c8d59 | ||
|
|
4dd917e303 | ||
|
|
16c08ba682 | ||
|
|
9b7f458b35 | ||
|
|
83f7babd89 | ||
|
|
0d65b5aaf9 | ||
|
|
ab90f57c8e | ||
|
|
6fbf238628 | ||
|
|
22e1e0bdb0 | ||
|
|
cc5683c5c4 | ||
|
|
d4e82d3909 | ||
|
|
35146cd1c8 | ||
|
|
3ad181950c | ||
|
|
78d07eb09f | ||
|
|
09970cd348 | ||
|
|
387b81d324 | ||
|
|
ae1cf98fc9 | ||
|
|
52354bf3f4 | ||
|
|
5bd317178a | ||
|
|
94bc2ce611 | ||
|
|
55287d4cc3 | ||
|
|
a8b72b046f | ||
|
|
69fce71ad5 | ||
|
|
e4e2657f17 | ||
|
|
56443bf3f8 | ||
|
|
258ab1cd20 | ||
|
|
63248a3a46 | ||
|
|
331b47013e | ||
|
|
162c10c16f | ||
|
|
cdefa98c09 | ||
|
|
5b2d924121 | ||
|
|
9fcf1b4acd | ||
|
|
7bec5453d0 | ||
|
|
7c87046753 | ||
|
|
81ea4d7706 | ||
|
|
f1d53a409b | ||
|
|
384bb06210 | ||
|
|
2dd1a80fde | ||
|
|
d4a1b6e64b | ||
|
|
09e85a24d2 | ||
|
|
5990367b3d | ||
|
|
01a20e307c | ||
|
|
ed3a689f8c | ||
|
|
7313b64544 | ||
|
|
dacb1dc42c | ||
|
|
74241c1459 | ||
|
|
b1048da6a5 | ||
|
|
ffc8cb1bbc | ||
|
|
eceaa96141 | ||
|
|
e632aadd42 | ||
|
|
bbcc505693 | ||
|
|
cc0544a4e3 | ||
|
|
b8455205f6 | ||
|
|
a10af4e839 | ||
|
|
ff37cbcf6f | ||
|
|
a42c6d325e | ||
|
|
724e4af963 | ||
|
|
7cc9718c41 | ||
|
|
72e477f4a3 | ||
|
|
ecfd858269 | ||
|
|
92c6690ab2 | ||
|
|
efb92fdd2e | ||
|
|
9c2f14e042 | ||
|
|
8b0567262d | ||
|
|
d80bff2d9a | ||
|
|
3e29936e86 | ||
|
|
34c6423dbc | ||
|
|
6b4e1b3590 | ||
|
|
7eb01489fd | ||
|
|
731e222a53 | ||
|
|
df50822ff6 | ||
|
|
7480c4389a | ||
|
|
494cd8b44a | ||
|
|
2e733e16d7 | ||
|
|
97141c75bd | ||
|
|
c660495911 | ||
|
|
0cb35a4fe3 | ||
|
|
3622b1a3d9 | ||
|
|
20d418e1de | ||
|
|
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 |
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
|
||||
@@ -67,6 +70,8 @@ runs:
|
||||
|
||||
- name: Install GitHub MCP Server
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||
run: |
|
||||
claude mcp add-json github '{
|
||||
"command": "docker",
|
||||
@@ -79,7 +84,7 @@ runs:
|
||||
"ghcr.io/github/github-mcp-server:sha-ff3036d"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ inputs.github_token }}"
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "'"${INPUT_GITHUB_TOKEN}"'"
|
||||
}
|
||||
}'
|
||||
|
||||
@@ -204,14 +209,44 @@ runs:
|
||||
User Request:
|
||||
$USER_REQUEST
|
||||
|
||||
Please analyze the request and provide helpful assistance. You have access to repository tools and can help with:
|
||||
- Code analysis and implementation
|
||||
- GitHub workflows and automation
|
||||
- Pull request reviews and feedback
|
||||
- Issue resolution and bug fixes
|
||||
- Documentation updates
|
||||
- Testing and deployment
|
||||
|
||||
Please analyze the request and provide helpful assistance.
|
||||
|
||||
CRITICAL INSTRUCTION: If your task involves modifying codebase files to add a feature or fix a bug:
|
||||
|
||||
STEP 0 — SETUP GIT IDENTITY:
|
||||
git config user.name "MervinPraison"
|
||||
git config user.email "[email protected]"
|
||||
|
||||
STEP 1 — READ GUIDELINES:
|
||||
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.
|
||||
|
||||
STEP 2 — ARCHITECTURE VALIDATION & ROUTING (MANDATORY before writing code):
|
||||
Before implementing anything, answer these questions:
|
||||
- CORE vs WRAPPER vs TOOLS ROUTING:
|
||||
1. Core SDK (praisonaiagents/): Only core protocols, base classes, decorators. No heavy implementations.
|
||||
2. Wrapper (praisonai/): CLI, heavy implementations, optional dependencies.
|
||||
3. Tools (PraisonAI-Tools): Specific integrations, external tools (e.g. SurrealDB, Slack), and community extensions MUST go to the `MervinPraison/PraisonAI-Tools` repository. If the feature is a tool, clone `https://github.com/MervinPraison/PraisonAI-Tools`, implement it there, and create the PR using `gh pr create -R MervinPraison/PraisonAI-Tools`.
|
||||
- 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 conceptual checks fail (excluding routing), add a comment to the issue explaining why and close it. Do NOT create a PR.
|
||||
|
||||
STEP 3 — IMPLEMENT:
|
||||
- Create a new git branch appropriately named (e.g. \`feature/issue-<number>\`)
|
||||
- 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 the branch
|
||||
- Use the GitHub CLI (\`gh pr create --title "..." --body "Fixes #<number>"\`) to open a Pull Request
|
||||
CRITICAL: You MUST create the PR automatically using \`gh pr create\`. Do NOT just provide a link.
|
||||
|
||||
Respond naturally and helpfully to the user's request using the available tools.
|
||||
EOF
|
||||
|
||||
@@ -221,6 +256,14 @@ runs:
|
||||
- name: Run Claude Code
|
||||
if: env.SKIP_EXECUTION != 'true'
|
||||
shell: bash
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
|
||||
GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||
INPUT_MAX_TURNS: ${{ inputs.max_turns }}
|
||||
INPUT_ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
|
||||
INPUT_DISALLOWED_TOOLS: ${{ inputs.disallowed_tools }}
|
||||
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
|
||||
run: |
|
||||
echo "🚀 Running Claude Code with GitHub context..."
|
||||
|
||||
@@ -228,29 +271,26 @@ runs:
|
||||
CMD_ARGS=("-p" "--verbose" "--output-format" "stream-json")
|
||||
|
||||
# Add max turns if specified
|
||||
if [[ -n "${{ inputs.max_turns }}" ]]; then
|
||||
CMD_ARGS+=("--max-turns" "${{ inputs.max_turns }}")
|
||||
if [[ -n "$INPUT_MAX_TURNS" ]]; then
|
||||
CMD_ARGS+=("--max-turns" "$INPUT_MAX_TURNS")
|
||||
fi
|
||||
|
||||
# Add allowed tools (include GitHub tools by default)
|
||||
TOOLS="mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues,mcp__github__create_comment,Read,Write,Edit,Bash"
|
||||
if [[ -n "${{ inputs.allowed_tools }}" ]]; then
|
||||
TOOLS="$TOOLS,${{ inputs.allowed_tools }}"
|
||||
if [[ -n "$INPUT_ALLOWED_TOOLS" ]]; then
|
||||
TOOLS="$TOOLS,$INPUT_ALLOWED_TOOLS"
|
||||
fi
|
||||
CMD_ARGS+=("--allowedTools" "$TOOLS")
|
||||
|
||||
# Add disallowed tools
|
||||
if [[ -n "${{ inputs.disallowed_tools }}" ]]; then
|
||||
CMD_ARGS+=("--disallowedTools" "${{ inputs.disallowed_tools }}")
|
||||
if [[ -n "$INPUT_DISALLOWED_TOOLS" ]]; then
|
||||
CMD_ARGS+=("--disallowedTools" "$INPUT_DISALLOWED_TOOLS")
|
||||
fi
|
||||
|
||||
echo "📝 Executing Claude Code with prompt from file..."
|
||||
|
||||
# Execute Claude Code with timeout, using stdin for the prompt
|
||||
TIMEOUT_SECONDS=$((${{ inputs.timeout_minutes }} * 60))
|
||||
timeout $TIMEOUT_SECONDS claude "${CMD_ARGS[@]}" < "${{ env.PROMPT_FILE }}"
|
||||
TIMEOUT_SECONDS=$(($INPUT_TIMEOUT_MINUTES * 60))
|
||||
timeout $TIMEOUT_SECONDS claude "${CMD_ARGS[@]}" < "$PROMPT_FILE"
|
||||
|
||||
echo "✅ Claude Code execution completed"
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||
GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||
echo "✅ Claude Code execution completed"
|
||||
@@ -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.
|
||||
@@ -51,13 +57,15 @@ runs:
|
||||
|
||||
- name: Install Gemini CLI
|
||||
id: install
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
if [[ "${{ inputs.version }}" == "latest" || "${{ inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.-]+)?(\+[a-zA-Z0-9\.-]+)?$ ]]; then
|
||||
echo "Installing Gemini CLI from npm: @google/gemini-cli@${{ inputs.version }}"
|
||||
npm install -g @google/gemini-cli@${{ inputs.version }}
|
||||
if [[ "$INPUT_VERSION" == "latest" || "$INPUT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.-]+)?(\+[a-zA-Z0-9\.-]+)?$ ]]; then
|
||||
echo "Installing Gemini CLI from npm: @google/gemini-cli@$INPUT_VERSION"
|
||||
npm install -g @google/gemini-cli@$INPUT_VERSION
|
||||
else
|
||||
echo "Installing Gemini CLI from GitHub: github:google-gemini/gemini-cli#${{ inputs.version }}"
|
||||
npm install -g github:google-gemini/gemini-cli#${{ inputs.version }}
|
||||
echo "Installing Gemini CLI from GitHub: github:google-gemini/gemini-cli#$INPUT_VERSION"
|
||||
npm install -g github:google-gemini/gemini-cli#$INPUT_VERSION
|
||||
fi
|
||||
echo "Verifying installation:"
|
||||
if command -v gemini >/dev/null 2>&1; then
|
||||
@@ -70,10 +78,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 |
@@ -0,0 +1,72 @@
|
||||
framework: "praisonai"
|
||||
process: workflow
|
||||
topic: "Autonomous Issue Resolution"
|
||||
planning: false
|
||||
|
||||
roles:
|
||||
issue_analyst:
|
||||
role: "GitHub Issue Analyst"
|
||||
goal: "Read the GitHub issue, understand the required changes, and map the problem to the local codebase"
|
||||
backstory: "You are an expert software analyst who reads bug reports and feature requests. You excel at taking a problem description and finding exactly which files in the codebase need to be modified. Your primary job is discovery and planning."
|
||||
tools:
|
||||
- "execute_command"
|
||||
|
||||
software_engineer:
|
||||
role: "Autonomous Software Engineer"
|
||||
goal: "Implement required code changes on a branch, run tests, commit, and push"
|
||||
backstory: "You are a senior software engineer. You always branch first, implement changes using shell commands, verify with git diff, run tests, then commit and push. You never create a Pull Request unless the issue body explicitly requests one."
|
||||
tools:
|
||||
- "execute_command"
|
||||
- "github_create_branch"
|
||||
- "github_commit_and_push"
|
||||
- "github_create_pull_request"
|
||||
|
||||
steps:
|
||||
- name: Analyze Issue
|
||||
agent: issue_analyst
|
||||
action: >
|
||||
Thoroughly analyze the issue and the repository. Follow these steps in order.
|
||||
|
||||
First, set up git identity: execute_command 'git config user.name PraisonAI' then
|
||||
execute_command 'git config user.email [email protected]'. Do NOT pass cwd.
|
||||
|
||||
Second, read the issue: execute_command 'cat .github/triage-context.md'. Do NOT pass cwd.
|
||||
|
||||
Third, read architecture guidelines: execute_command 'cat src/praisonai-agents/AGENTS.md'.
|
||||
|
||||
Fourth, decide if this issue requires code changes:
|
||||
- If YES: identify the exact files and lines that must change. Output a concrete file-level plan.
|
||||
- If NO (question, discussion, duplicate, invalid): say so clearly. No code changes needed.
|
||||
|
||||
Output: routing decision (Core SDK / Wrapper / Tools / No-change), validation result, and exact files/lines to change.
|
||||
expected_output: A decision on whether code changes are needed, and if so a concrete file-level plan.
|
||||
|
||||
- name: Implement Fix
|
||||
agent: software_engineer
|
||||
action: >
|
||||
You are working inside the repository at /Users/praison/praisonai-package.
|
||||
Do NOT ask for confirmation. Do NOT use placeholder paths. Act immediately.
|
||||
|
||||
Read the analyst's output first. If the analyst said NO code changes are needed, stop here and
|
||||
output a brief explanation — do NOT create a branch or PR.
|
||||
|
||||
If code changes ARE needed, follow these steps:
|
||||
|
||||
Step 1 — Create branch: github_create_branch with branch_name=praisonai/issue-{{ISSUE_NUMBER}}.
|
||||
|
||||
Step 2 — Implement changes using execute_command (no cwd argument).
|
||||
To insert a line: python3 -c "lines=open('FILE').readlines(); lines.insert(N,'LINE\n'); open('FILE','w').writelines(lines)"
|
||||
To replace text: python3 -c "t=open('FILE').read(); open('FILE','w').write(t.replace('OLD','NEW'))"
|
||||
After each edit verify: execute_command 'git diff'
|
||||
|
||||
Step 3 — Run tests: execute_command 'python3 -m pytest src/praisonai-agents/tests/ -x -q --timeout=30 -p no:warnings'
|
||||
|
||||
Step 4 — Commit and push: github_commit_and_push with a descriptive commit_message.
|
||||
|
||||
Step 5 — Check if the issue body EXPLICITLY contains the phrase "create pull request" or "create PR"
|
||||
(case-insensitive). ONLY in that case create a PR using github_create_pull_request with
|
||||
title='fix: resolve issue #{{ISSUE_NUMBER}}',
|
||||
body='Fixes #{{ISSUE_NUMBER}}.\n\nAutonomously resolved by PraisonAI.',
|
||||
head_branch=praisonai/issue-{{ISSUE_NUMBER}}, base_branch=main.
|
||||
Otherwise do NOT create a PR — just confirm the branch was pushed successfully.
|
||||
expected_output: Either "No code changes needed", or "Branch pushed, no PR requested", or "Branch pushed and PR created at URL".
|
||||
@@ -0,0 +1,116 @@
|
||||
framework: "praisonai"
|
||||
topic: "Pull Request Code Review Analysis"
|
||||
|
||||
roles:
|
||||
security_reviewer:
|
||||
role: "Security Code Reviewer"
|
||||
goal: "Identify security vulnerabilities, authentication issues, input validation problems, and unsafe code practices in the PR changes"
|
||||
backstory: "You are a cybersecurity expert specializing in code analysis. You have extensive experience in identifying common security flaws like injection attacks, authentication bypasses, exposed secrets, and unsafe data handling patterns."
|
||||
tools:
|
||||
- "execute_command"
|
||||
|
||||
performance_reviewer:
|
||||
role: "Performance Code Reviewer"
|
||||
goal: "Analyze code changes for performance implications, identify bottlenecks, inefficient algorithms, and resource usage issues"
|
||||
backstory: "You are a performance optimization specialist with deep knowledge of algorithm efficiency, memory management, and system performance. You excel at spotting code that could cause performance degradation."
|
||||
tools:
|
||||
- "execute_command"
|
||||
|
||||
maintainability_reviewer:
|
||||
role: "Code Quality & Maintainability Reviewer"
|
||||
goal: "Evaluate code structure, readability, documentation, naming conventions, and adherence to best practices"
|
||||
backstory: "You are a software engineering expert focused on code quality and maintainability. You ensure code follows established patterns, is well-documented, and will be easy for future developers to understand and modify."
|
||||
tools:
|
||||
- "execute_command"
|
||||
|
||||
lead_reviewer:
|
||||
role: "Lead Technical Reviewer"
|
||||
goal: "Synthesize all review feedback, make final recommendations, and post comprehensive review comments to the GitHub PR"
|
||||
backstory: "You are a senior technical lead responsible for final review decisions. You coordinate input from security, performance, and maintainability reviewers to provide balanced, actionable feedback to the development team."
|
||||
tools:
|
||||
- "execute_command"
|
||||
|
||||
steps:
|
||||
- name: security_analysis
|
||||
agent: security_reviewer
|
||||
action: |
|
||||
Analyze the PR diff for security issues:
|
||||
1. Extract PR details: `echo "$PR_DATA"`
|
||||
2. Get changed files: `echo "$CHANGED_FILES"`
|
||||
3. Review full diff: `gh pr diff ${PR_NUMBER}`
|
||||
4. Look for:
|
||||
- Hardcoded secrets, API keys, passwords
|
||||
- SQL injection vulnerabilities
|
||||
- XSS vulnerabilities
|
||||
- Authentication/authorization bypasses
|
||||
- Unsafe file operations
|
||||
- Command injection risks
|
||||
- Missing input validation
|
||||
- Exposed sensitive data
|
||||
5. Document findings with file paths and line numbers
|
||||
expected_output: "Detailed security analysis report with specific vulnerabilities found, severity levels, and remediation suggestions"
|
||||
|
||||
- name: performance_analysis
|
||||
agent: performance_reviewer
|
||||
action: |
|
||||
Analyze the PR diff for performance issues:
|
||||
1. Review algorithm complexity changes
|
||||
2. Check for inefficient database queries
|
||||
3. Identify memory leaks or excessive allocations
|
||||
4. Look for hot-path regressions
|
||||
5. Check for blocking I/O operations
|
||||
6. Analyze module-level imports that could slow startup
|
||||
7. Review caching implementations
|
||||
8. Check for recursive operations without termination
|
||||
9. Document performance concerns with impact estimates
|
||||
expected_output: "Performance analysis report highlighting bottlenecks, efficiency concerns, and optimization recommendations"
|
||||
|
||||
- name: maintainability_analysis
|
||||
agent: maintainability_reviewer
|
||||
action: |
|
||||
Analyze the PR diff for code quality and maintainability:
|
||||
1. Check naming conventions consistency
|
||||
2. Evaluate code structure and organization
|
||||
3. Review documentation completeness
|
||||
4. Check for DRY violations
|
||||
5. Analyze error handling patterns
|
||||
6. Review test coverage for changes
|
||||
7. Check adherence to repository coding standards
|
||||
8. Evaluate API design consistency
|
||||
9. Look for technical debt introduction
|
||||
expected_output: "Code quality analysis with specific recommendations for improving maintainability and readability"
|
||||
|
||||
- name: final_review
|
||||
agent: lead_reviewer
|
||||
action: |
|
||||
Synthesize all reviews and post final comprehensive feedback:
|
||||
1. Collect input from security, performance, and maintainability reviews
|
||||
2. Prioritize findings by severity (Critical, High, Medium, Low)
|
||||
3. Create structured review comment using this format:
|
||||
|
||||
## 📋 Review Summary
|
||||
[2-3 sentence overview and assessment]
|
||||
|
||||
## 🔍 General Feedback
|
||||
[Overall patterns and observations]
|
||||
|
||||
## 🎯 Specific Feedback
|
||||
### 🔴 Critical (if any)
|
||||
[Security vulnerabilities, breaking changes, major bugs]
|
||||
|
||||
### 🟡 High (if any)
|
||||
[Performance issues, design flaws, significant bugs]
|
||||
|
||||
### 🟢 Medium (if any)
|
||||
[Code quality improvements, minor optimizations]
|
||||
|
||||
### 🔵 Low (if any)
|
||||
[Documentation, naming suggestions, minor refactoring]
|
||||
|
||||
## ✅ Highlights (if any)
|
||||
[Positive aspects worth mentioning]
|
||||
|
||||
4. Post comprehensive review: `echo "[REVIEW_CONTENT]" | gh pr comment ${PR_NUMBER} -F -`
|
||||
5. If critical issues found, suggest not merging until resolved
|
||||
expected_output: "Final review comment posted to GitHub PR with comprehensive analysis from all reviewers"
|
||||
dependencies: [security_analysis, performance_analysis, maintainability_analysis]
|
||||
@@ -0,0 +1,3 @@
|
||||
# GitHub Issue #1350: Add debug log on successful branch checkout
|
||||
|
||||
The github_create_branch function in src/praisonai-agents/praisonaiagents/tools/github_tools.py should call logger.debug after a successful checkout.
|
||||
@@ -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,445 @@
|
||||
name: Auto PR Comment
|
||||
|
||||
# Review chain: CodeRabbit/Qodo/Gemini review first → Copilot → Claude (final)
|
||||
# For human PRs: CodeRabbit/Qodo auto-review → this workflow triggers Copilot → then Claude
|
||||
# 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).
|
||||
#
|
||||
# Claude trigger runs IN-BAND (same workflow) to avoid GitHub Actions
|
||||
# blocking bot-triggered workflows with action_required approval gates.
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
pull_request:
|
||||
types: [opened]
|
||||
|
||||
env:
|
||||
MAX_POLL_ATTEMPTS: 20
|
||||
POLL_DELAY_MS: 30000
|
||||
CLAUDE_TRIGGER_LOGINS: '["MervinPraison","github-actions[bot]"]'
|
||||
|
||||
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
|
||||
outputs:
|
||||
pr_number: ${{ steps.trigger.outputs.pr_number }}
|
||||
triggered: ${{ steps.trigger.outputs.triggered }}
|
||||
steps:
|
||||
- name: Add automated comment
|
||||
- name: Post Copilot review request (as user via PAT)
|
||||
id: trigger
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
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');
|
||||
core.setOutput('triggered', 'false');
|
||||
core.setOutput('pr_number', context.issue.number.toString());
|
||||
return;
|
||||
}
|
||||
await 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'
|
||||
});
|
||||
core.setOutput('triggered', 'true');
|
||||
core.setOutput('pr_number', context.issue.number.toString());
|
||||
|
||||
# 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
|
||||
outputs:
|
||||
pr_number: ${{ steps.trigger.outputs.pr_number }}
|
||||
triggered: ${{ steps.trigger.outputs.triggered }}
|
||||
steps:
|
||||
- name: Post Copilot review request (as user via PAT)
|
||||
id: trigger
|
||||
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');
|
||||
core.setOutput('triggered', 'false');
|
||||
core.setOutput('pr_number', context.issue.number.toString());
|
||||
return;
|
||||
}
|
||||
await 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'
|
||||
});
|
||||
core.setOutput('triggered', 'true');
|
||||
core.setOutput('pr_number', context.issue.number.toString());
|
||||
|
||||
# ================================================================
|
||||
# Claude FINAL review — polls for Copilot's response, then triggers
|
||||
# This runs IN-BAND under the PAT context (MervinPraison), avoiding
|
||||
# the action_required approval gate that blocks bot-triggered workflows.
|
||||
# ================================================================
|
||||
claude-after-copilot:
|
||||
needs: [copilot-after-coderabbit]
|
||||
if: |
|
||||
always() &&
|
||||
needs.copilot-after-coderabbit.result == 'success' &&
|
||||
needs.copilot-after-coderabbit.outputs.triggered == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Wait for Copilot to respond (poll)
|
||||
id: poll
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
|
||||
MAX_POLL_ATTEMPTS: ${{ env.MAX_POLL_ATTEMPTS }}
|
||||
POLL_DELAY_MS: ${{ env.POLL_DELAY_MS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
// Poll for Copilot's response — check every 30s for up to 10 minutes
|
||||
const maxAttempts = parseInt(process.env.MAX_POLL_ATTEMPTS, 10);
|
||||
const delayMs = parseInt(process.env.POLL_DELAY_MS, 10);
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
console.log(`Poll attempt ${i + 1}/${maxAttempts}...`);
|
||||
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: pr,
|
||||
owner,
|
||||
repo,
|
||||
per_page: 50,
|
||||
sort: 'created',
|
||||
direction: 'desc'
|
||||
});
|
||||
|
||||
// Look for Copilot's response (copilot-swe-agent or Copilot)
|
||||
const copilotResponse = comments.data.find(c =>
|
||||
(c.user.login === 'copilot-swe-agent' ||
|
||||
c.user.login === 'Copilot' ||
|
||||
c.user.login.toLowerCase().includes('copilot')) &&
|
||||
c.user.type === 'Bot'
|
||||
);
|
||||
|
||||
if (copilotResponse) {
|
||||
console.log(`Copilot responded at ${copilotResponse.created_at}`);
|
||||
core.setOutput('copilot_responded', 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
// Also check PR reviews for Copilot
|
||||
const reviews = await github.rest.pulls.listReviews({
|
||||
pull_number: pr,
|
||||
owner,
|
||||
repo
|
||||
});
|
||||
|
||||
const copilotReview = reviews.data.find(r =>
|
||||
r.user.login.toLowerCase().includes('copilot')
|
||||
);
|
||||
|
||||
if (copilotReview) {
|
||||
console.log(`Copilot review submitted at ${copilotReview.submitted_at}`);
|
||||
core.setOutput('copilot_responded', 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
if (i < maxAttempts - 1) {
|
||||
console.log(`Copilot hasn't responded yet, waiting ${delayMs/1000}s...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Copilot did not respond within timeout, triggering Claude anyway');
|
||||
core.setOutput('copilot_responded', 'timeout');
|
||||
|
||||
- name: Check if Claude already triggered
|
||||
id: check_claude
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
|
||||
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: pr,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c =>
|
||||
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
|
||||
);
|
||||
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
|
||||
|
||||
- name: Trigger Claude final review
|
||||
if: steps.check_claude.outputs.already_triggered != 'true'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: pr,
|
||||
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'
|
||||
});
|
||||
console.log(`Claude triggered on PR #${pr}`);
|
||||
|
||||
# ================================================================
|
||||
# Claude after Qodo fallback path (same polling approach)
|
||||
# ================================================================
|
||||
claude-after-copilot-qodo:
|
||||
needs: [copilot-after-qodo]
|
||||
if: |
|
||||
always() &&
|
||||
needs.copilot-after-qodo.result == 'success' &&
|
||||
needs.copilot-after-qodo.outputs.triggered == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Wait for Copilot to respond (poll)
|
||||
id: poll
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
|
||||
MAX_POLL_ATTEMPTS: ${{ env.MAX_POLL_ATTEMPTS }}
|
||||
POLL_DELAY_MS: ${{ env.POLL_DELAY_MS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const maxAttempts = parseInt(process.env.MAX_POLL_ATTEMPTS, 10);
|
||||
const delayMs = parseInt(process.env.POLL_DELAY_MS, 10);
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
console.log(`Poll attempt ${i + 1}/${maxAttempts}...`);
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: pr, owner, repo,
|
||||
per_page: 50, sort: 'created', direction: 'desc'
|
||||
});
|
||||
const copilotResponse = comments.data.find(c =>
|
||||
(c.user.login === 'copilot-swe-agent' ||
|
||||
c.user.login === 'Copilot' ||
|
||||
c.user.login.toLowerCase().includes('copilot')) &&
|
||||
c.user.type === 'Bot'
|
||||
);
|
||||
if (copilotResponse) {
|
||||
console.log(`Copilot responded at ${copilotResponse.created_at}`);
|
||||
core.setOutput('copilot_responded', 'true');
|
||||
return;
|
||||
}
|
||||
const reviews = await github.rest.pulls.listReviews({
|
||||
pull_number: pr, owner, repo
|
||||
});
|
||||
const copilotReview = reviews.data.find(r =>
|
||||
r.user.login.toLowerCase().includes('copilot')
|
||||
);
|
||||
if (copilotReview) {
|
||||
console.log(`Copilot review submitted at ${copilotReview.submitted_at}`);
|
||||
core.setOutput('copilot_responded', 'true');
|
||||
return;
|
||||
}
|
||||
if (i < maxAttempts - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
console.log('Copilot did not respond within timeout, triggering Claude anyway');
|
||||
core.setOutput('copilot_responded', 'timeout');
|
||||
|
||||
- name: Check if Claude already triggered
|
||||
id: check_claude
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
|
||||
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
|
||||
const comments = await github.rest.issues.listComments({
|
||||
issue_number: pr,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const alreadyPosted = comments.data.some(c =>
|
||||
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
|
||||
);
|
||||
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
|
||||
|
||||
- name: Trigger Claude final review
|
||||
if: steps.check_claude.outputs.already_triggered != 'true'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const pr = parseInt(process.env.PR_NUMBER, 10);
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: pr,
|
||||
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'
|
||||
});
|
||||
console.log(`Claude triggered on PR #${pr}`);
|
||||
|
||||
# ================================================================
|
||||
# Claude after Gemini — restores the trigger lost from the deleted
|
||||
# chain-claude-after-copilot.yml workflow.
|
||||
# Fires when Gemini posts a review comment on a PR or issue.
|
||||
# ================================================================
|
||||
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: Check if Claude already triggered
|
||||
id: check_claude
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
|
||||
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('@claude') && triggerLogins.includes(c.user.login)
|
||||
);
|
||||
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
|
||||
|
||||
- name: Post Claude analysis request (Lead Engineer)
|
||||
if: steps.check_claude.outputs.already_triggered != 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
await 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'
|
||||
});
|
||||
|
||||
# ================================================================
|
||||
# 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 and Qodo 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 (commented out)
|
||||
// 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.');
|
||||
@@ -35,6 +35,8 @@ jobs:
|
||||
|
||||
- name: Run Import Time Benchmark
|
||||
id: import_time
|
||||
env:
|
||||
FAIL_ON_REGRESSION: ${{ inputs.fail_on_regression }}
|
||||
run: |
|
||||
cd src/praisonai-agents
|
||||
python benchmarks/import_time.py 2>&1 | tee import_time_output.txt
|
||||
@@ -46,7 +48,7 @@ jobs:
|
||||
# Check threshold (200ms)
|
||||
if (( $(echo "$IMPORT_TIME > 200" | bc -l) )); then
|
||||
echo "::warning::Import time ${IMPORT_TIME}ms exceeds 200ms threshold"
|
||||
if [ "${{ inputs.fail_on_regression }}" == "true" ]; then
|
||||
if [ "$FAIL_ON_REGRESSION" == "true" ]; then
|
||||
echo "import_passed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
@@ -55,6 +57,8 @@ jobs:
|
||||
|
||||
- name: Run Memory Usage Benchmark
|
||||
id: memory
|
||||
env:
|
||||
FAIL_ON_REGRESSION: ${{ inputs.fail_on_regression }}
|
||||
run: |
|
||||
cd src/praisonai-agents
|
||||
python benchmarks/memory_usage.py 2>&1 | tee memory_output.txt
|
||||
@@ -66,7 +70,7 @@ jobs:
|
||||
# Check threshold (30MB)
|
||||
if (( $(echo "$PEAK_MEMORY > 30" | bc -l) )); then
|
||||
echo "::warning::Memory usage ${PEAK_MEMORY}MB exceeds 30MB threshold"
|
||||
if [ "${{ inputs.fail_on_regression }}" == "true" ]; then
|
||||
if [ "$FAIL_ON_REGRESSION" == "true" ]; then
|
||||
echo "memory_passed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
@@ -95,9 +99,11 @@ jobs:
|
||||
|
||||
- name: Run Parameter Impact Benchmark
|
||||
id: param_impact
|
||||
env:
|
||||
INPUT_ITERATIONS: ${{ inputs.iterations }}
|
||||
run: |
|
||||
cd src/praisonai-agents
|
||||
python benchmarks/param_impact_benchmark.py --no-save --iterations ${{ inputs.iterations }} 2>&1 | tee param_impact_output.txt
|
||||
python benchmarks/param_impact_benchmark.py --no-save --iterations "$INPUT_ITERATIONS" 2>&1 | tee param_impact_output.txt
|
||||
|
||||
- name: Check Lazy Imports
|
||||
id: lazy_imports
|
||||
|
||||
@@ -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,23 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
id-token: write
|
||||
steps:
|
||||
- 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 }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- 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,262 @@ 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)
|
||||
# ================================================================
|
||||
# Job 1: Lightweight triage for ALL new issues (including external users)
|
||||
# Posts welcome comment + adds triage labels. No Claude code execution.
|
||||
# ================================================================
|
||||
issue-triage:
|
||||
if: |
|
||||
github.event_name == 'issues' &&
|
||||
github.event.action == 'opened' &&
|
||||
!contains(toJSON(github.event.issue.labels), 'claude')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
steps:
|
||||
- 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 }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Triage and acknowledge issue
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const title = (issue.title || '').toLowerCase();
|
||||
const body = (issue.body || '').toLowerCase();
|
||||
const content = title + ' ' + body;
|
||||
const isOwner = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(
|
||||
issue.author_association
|
||||
);
|
||||
|
||||
// --- Label triage ---
|
||||
const labels = [];
|
||||
|
||||
// Kind labels
|
||||
if (content.includes('feature request') || content.includes('[feature')) {
|
||||
labels.push('enhancement');
|
||||
} else if (content.includes('bug') || content.includes('error') || content.includes('crash') || content.includes('traceback')) {
|
||||
labels.push('bug');
|
||||
} else if (content.includes('question') || content.includes('how do i') || content.includes('how to')) {
|
||||
labels.push('question');
|
||||
}
|
||||
|
||||
// Area labels
|
||||
if (content.includes('security') || content.includes('cve') || content.includes('vulnerability')) {
|
||||
labels.push('security');
|
||||
}
|
||||
if (content.includes('performance') || content.includes('slow') || content.includes('memory leak')) {
|
||||
labels.push('performance');
|
||||
}
|
||||
if (content.includes('documentation') || content.includes('docs')) {
|
||||
labels.push('documentation');
|
||||
}
|
||||
if (content.includes('typescript') || content.includes('javascript') || content.includes('npm')) {
|
||||
labels.push('javascript');
|
||||
}
|
||||
|
||||
// Apply labels
|
||||
if (labels.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
issue_number: issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels
|
||||
});
|
||||
}
|
||||
|
||||
// --- Acknowledgment comment (external users only) ---
|
||||
if (!isOwner) {
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: [
|
||||
`👋 Thanks for opening this issue, @${issue.user.login}!`,
|
||||
'',
|
||||
'A maintainer will review this shortly. In the meantime:',
|
||||
'- Make sure you\'ve included steps to reproduce (for bugs)',
|
||||
'- Check [existing issues](https://github.com/MervinPraison/PraisonAI/issues) for duplicates',
|
||||
'- Review the [documentation](https://docs.praison.ai) for related guides',
|
||||
'',
|
||||
'_A maintainer can trigger deeper analysis by commenting `@claude` on this issue._'
|
||||
].join('\n')
|
||||
});
|
||||
}
|
||||
|
||||
// --- For owner: add 'claude' label to trigger the code-fix job ---
|
||||
if (isOwner) {
|
||||
await github.rest.issues.addLabels({
|
||||
issue_number: issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['claude']
|
||||
});
|
||||
}
|
||||
|
||||
# ================================================================
|
||||
# Job 2: Full Claude code-fix (owner/collaborator only)
|
||||
# Triggers on: labeled 'claude', @claude comments, assignments
|
||||
# ================================================================
|
||||
claude-response:
|
||||
# Allow: human users, github-actions[bot] (auto-comments/labels)
|
||||
# Block: dependabot, cursor, renovate, other bots
|
||||
# Skip 'opened' events — triage job handles those above
|
||||
if: |
|
||||
github.event.action != 'opened' &&
|
||||
(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: 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 }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Check for Fork PR
|
||||
id: check_fork
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const isPR = context.eventName === 'pull_request' || (context.payload.issue && context.payload.issue.pull_request);
|
||||
if (isPR) {
|
||||
const prNumber = context.payload.pull_request ? context.payload.pull_request.number : context.payload.issue.number;
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
core.setOutput('pr_branch', pr.data.head.ref);
|
||||
|
||||
if (pr.data.head.repo && pr.data.head.repo.full_name !== context.repo.full_name) {
|
||||
core.setOutput('is_fork', 'true');
|
||||
core.setOutput('clone_url', pr.data.head.repo.clone_url);
|
||||
core.setOutput('branch', pr.data.head.ref);
|
||||
return;
|
||||
}
|
||||
}
|
||||
core.setOutput('is_fork', 'false');
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Fetch PR branch and Setup Remote
|
||||
if: github.event.issue.pull_request
|
||||
run: |
|
||||
# Fetch PR head from base repo and put it into local branch
|
||||
git fetch origin pull/${{ github.event.issue.number }}/head:${{ steps.check_fork.outputs.pr_branch }}
|
||||
|
||||
# If it's a fork, make origin point to local to trick claude-code-action's `git fetch origin <branch>`
|
||||
if [ "${{ steps.check_fork.outputs.is_fork }}" == "true" ]; then
|
||||
git remote set-url origin file://$(pwd)
|
||||
fi
|
||||
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_ACCESS_TOKEN }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowed_bots: 'praisonai-triage-agent[bot]'
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
github_token: ${{ steps.app-token.outputs.token }}
|
||||
trigger_phrase: "@claude"
|
||||
label_trigger: "claude"
|
||||
|
||||
direct_prompt: |
|
||||
${{ steps.check_fork.outputs.is_fork == 'true' && 'CRITICAL: THIS IS A PULL REQUEST FROM A FORK. YOU DO NOT HAVE PUSH PERMISSIONS TO THIS REPOSITORY. ONLY READ FILES, VALIDATE CODE, AND PROVIDE YOUR COMMENTS/FEEDBACK DIRECTLY IN THIS PR VIA COMMENTS. DO NOT ATTEMPT TO PUSH OR CREATE PULL REQUESTS.\n\n' || '' }}
|
||||
You are working on the PraisonAI SDK. Follow AGENTS.md strictly.
|
||||
|
||||
STEP 0 — SETUP GIT IDENTITY & AUTH (GLOBAL — required for all repos):
|
||||
git config --global user.name "MervinPraison"
|
||||
git config --global user.email "[email protected]"
|
||||
gh auth setup-git
|
||||
|
||||
|
||||
STEP 1 — READ GUIDELINES:
|
||||
Read AGENTS.md to understand the architecture rules.
|
||||
|
||||
STEP 2 — ARCHITECTURE VALIDATION & ROUTING (MANDATORY before writing code):
|
||||
Before implementing anything, answer these questions:
|
||||
- CORE vs WRAPPER vs TOOLS vs DOCS vs PLUGINS ROUTING:
|
||||
1. Core SDK (praisonaiagents/): Only core protocols, base classes, decorators. No heavy implementations.
|
||||
2. Wrapper (praisonai/): CLI, heavy implementations, optional dependencies.
|
||||
3. Tools (PraisonAI-Tools): Capabilities Agents actively call during execution (e.g. SurrealDB tool, Slack tool).
|
||||
4. Documentation (PraisonAIDocs): Documentation pages, guides, API references.
|
||||
5. Plugins (PraisonAI-Plugins): Framework lifecycle extensions replacing systemic behavior (e.g. tracing, logging, metrics, hooks, guardrails).
|
||||
For items routed to an EXTERNAL repository (Tools, Docs, Plugins), follow STEP 3-ALT below.
|
||||
- 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 conceptual checks fail (excluding routing), add a comment to the issue explaining why and close it. Do NOT create a PR.
|
||||
|
||||
STEP 3 — IMPLEMENT (for changes in THIS repo — PraisonAI):
|
||||
- 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 3-ALT — IMPLEMENT IN EXTERNAL REPO (for PraisonAI-Tools, PraisonAIDocs, PraisonAI-Plugins):
|
||||
When work must happen in a different repository, follow these steps EXACTLY. Do NOT attempt to use `cd`, as directory state is not preserved. Use `git -C` for all commands.
|
||||
a) Clone the repository:
|
||||
gh repo clone MervinPraison/<REPO_NAME> /tmp/<REPO_NAME>
|
||||
b) Copy GitHub authentication from the main repository so push works seamlessly:
|
||||
git -C /tmp/<REPO_NAME> config http."https://github.com/".extraheader "$(git config --get http."https://github.com/".extraheader)"
|
||||
c) Create a feature branch (NEVER commit to main):
|
||||
git -C /tmp/<REPO_NAME> checkout -b claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)
|
||||
d) Make your changes using absolute paths (e.g. edit /tmp/<REPO_NAME>/docs.json)
|
||||
e) Commit:
|
||||
git -C /tmp/<REPO_NAME> add -A
|
||||
git -C /tmp/<REPO_NAME> commit -m "feat: <description> (fixes #$ISSUE_NUMBER)"
|
||||
f) Push the branch:
|
||||
git -C /tmp/<REPO_NAME> push origin claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d)
|
||||
g) Create PR in the EXTERNAL repo:
|
||||
gh pr create -R MervinPraison/<REPO_NAME> --head claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d) --title "feat: <title>" --body "Fixes MervinPraison/PraisonAI#$ISSUE_NUMBER
|
||||
|
||||
<body>"
|
||||
|
||||
STEP 4 — TEST:
|
||||
- For SDK changes: cd src/praisonai-agents && PYTHONPATH=. python -m pytest tests/ -x -q --timeout=30
|
||||
- For docs changes: verify files exist and are valid markdown/MDX
|
||||
- Ensure no regressions
|
||||
|
||||
STEP 5 — CREATE PR:
|
||||
- Commit with descriptive message, push, and create PR using `gh pr create`
|
||||
- For external repos, use `gh pr create -R MervinPraison/<REPO_NAME>`
|
||||
CRITICAL: You MUST create the PR automatically using `gh pr create`. Do NOT just provide a link or say "manual push required".
|
||||
NOTE: If you worked on an external repo, the wrapper action will report "No commits" for the main repo at the end. This is expected, do not worry about it.
|
||||
allowed_tools: |
|
||||
Bash(git:*)
|
||||
Bash(python:*)
|
||||
@@ -55,106 +282,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`
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
name: PraisonAI Issue Triage
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'Issue Number'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
!github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '@praisonai') &&
|
||||
github.actor != 'github-actions[bot]' &&
|
||||
github.event.comment.user.type != 'Bot' &&
|
||||
(github.event.comment.author_association == 'OWNER' ||
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR'))
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.PRAISONAI_APP_ID }}
|
||||
private_key: ${{ secrets.PRAISONAI_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Set up Git
|
||||
run: |
|
||||
git config --local user.email "[email protected]"
|
||||
git config --local user.name "PraisonAI Bot"
|
||||
|
||||
- name: Install PraisonAI
|
||||
run: |
|
||||
pip install uv
|
||||
cd src/praisonai-agents && uv pip install --system -e ".[all]"
|
||||
cd ../praisonai && uv pip install --system -e ".[all]"
|
||||
|
||||
- name: Run PraisonAI Issue Triage
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
PRAISONAI_AUTO_APPROVE: "true"
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
export ISSUE_NUMBER="${{ inputs.issue_number }}"
|
||||
else
|
||||
export ISSUE_NUMBER="${{ github.event.issue.number }}"
|
||||
fi
|
||||
praisonai github triage --issue $ISSUE_NUMBER --agent-file .github/praisonai-issue-triage.yaml
|
||||
@@ -0,0 +1,68 @@
|
||||
name: PraisonAI PR Reviewer
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'Pull Request Number'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '@praisonai') &&
|
||||
github.actor != 'github-actions[bot]' &&
|
||||
github.event.comment.user.type != 'Bot' &&
|
||||
(github.event.comment.author_association == 'OWNER' ||
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR'))
|
||||
|
||||
steps:
|
||||
- name: Determine checkout ref
|
||||
id: dest
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "issue_comment" ]; then
|
||||
echo "ref=refs/pull/${{ github.event.issue.number }}/head" >> "$GITHUB_OUTPUT"
|
||||
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "ref=refs/pull/${{ inputs.pr_number }}/head" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ref=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.dest.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.PRAISONAI_APP_ID }}
|
||||
private_key: ${{ secrets.PRAISONAI_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install PraisonAI
|
||||
run: pip install "praisonai[all]"
|
||||
|
||||
- name: Run PraisonAI PR Review
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
run: |
|
||||
praisonai workflow run .github/praisonai-reviewer.yaml
|
||||
@@ -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"
|
||||
@@ -34,10 +34,12 @@ jobs:
|
||||
# Get the commit hash from the tag
|
||||
- name: Get commit hash
|
||||
id: get_commit
|
||||
env:
|
||||
INPUT_TAG: ${{ github.event.inputs.tag }}
|
||||
run: |
|
||||
COMMIT_HASH=$(git rev-parse HEAD)
|
||||
echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT
|
||||
echo "Found commit: $COMMIT_HASH for tag ${{ github.event.inputs.tag }}"
|
||||
echo "Found commit: $COMMIT_HASH for tag $INPUT_TAG"
|
||||
|
||||
# Now checkout the fork at main branch
|
||||
- name: Checkout fork
|
||||
@@ -51,8 +53,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
|
||||
@@ -60,8 +62,11 @@ jobs:
|
||||
# Fetch from upstream
|
||||
git fetch upstream --tags --force
|
||||
|
||||
# Reset to upstream commit
|
||||
git reset --hard ${{ steps.get_commit.outputs.commit_hash }}
|
||||
- name: Hard reset to tag commit
|
||||
env:
|
||||
COMMIT_HASH: ${{ steps.get_commit.outputs.commit_hash }}
|
||||
run: |
|
||||
git reset --hard $COMMIT_HASH
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -445,55 +450,64 @@ jobs:
|
||||
# Add upstream and get commit from tag
|
||||
- name: Get upstream tag commit
|
||||
id: get_upstream_commit
|
||||
env:
|
||||
INPUT_TAG: ${{ github.event.inputs.tag }}
|
||||
run: |
|
||||
# Add upstream remote
|
||||
git remote add upstream https://github.com/MervinPraison/PraisonAI.git
|
||||
|
||||
# Delete local tag if it exists
|
||||
git tag -d ${{ github.event.inputs.tag }} || true
|
||||
git tag -d "$INPUT_TAG" || true
|
||||
|
||||
# Fetch from upstream including tags with force
|
||||
git fetch upstream --tags --force
|
||||
|
||||
# Verify tag exists in upstream
|
||||
if ! git ls-remote --tags upstream | grep -q "refs/tags/${{ github.event.inputs.tag }}$"; then
|
||||
echo "::error::Tag ${{ github.event.inputs.tag }} not found in upstream repository"
|
||||
if ! git ls-remote --tags upstream | grep -q "refs/tags/$INPUT_TAG$"; then
|
||||
echo "::error::Tag $INPUT_TAG not found in upstream repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the commit hash from the upstream tag
|
||||
UPSTREAM_COMMIT=$(git rev-list -n 1 refs/tags/${{ github.event.inputs.tag }})
|
||||
UPSTREAM_COMMIT=$(git rev-list -n 1 refs/tags/"$INPUT_TAG")
|
||||
if [ -z "$UPSTREAM_COMMIT" ]; then
|
||||
echo "::error::Could not find commit for tag ${{ github.event.inputs.tag }}"
|
||||
echo "::error::Could not find commit for tag $INPUT_TAG"
|
||||
exit 1
|
||||
fi
|
||||
echo "upstream_commit=$UPSTREAM_COMMIT" >> $GITHUB_OUTPUT
|
||||
echo "Found commit: $UPSTREAM_COMMIT for tag ${{ github.event.inputs.tag }}"
|
||||
echo "Found commit: $UPSTREAM_COMMIT for tag $INPUT_TAG"
|
||||
|
||||
# Create tag at the same commit
|
||||
- name: Create tag at commit
|
||||
run: |
|
||||
git config --global user.name "GitHub Actions"
|
||||
git config --global user.email "[email protected]"
|
||||
|
||||
# Remove tag if it exists locally
|
||||
git tag -d ${{ github.event.inputs.tag }} || true
|
||||
|
||||
# Remove tag if it exists remotely
|
||||
git push origin :refs/tags/${{ github.event.inputs.tag }} || true
|
||||
|
||||
# Fetch the specific commit from upstream
|
||||
git fetch upstream ${{ steps.get_upstream_commit.outputs.upstream_commit }} --force
|
||||
|
||||
# Reset to upstream commit
|
||||
git reset --hard ${{ steps.get_upstream_commit.outputs.upstream_commit }}
|
||||
|
||||
# Create and push tag
|
||||
git tag -a ${{ github.event.inputs.tag }} -m "Release ${{ github.event.inputs.tag }} (synced from upstream MervinPraison/PraisonAI@${{ github.event.inputs.tag }})"
|
||||
git push origin ${{ github.event.inputs.tag }}
|
||||
env:
|
||||
GIT_ASKPASS: /bin/echo
|
||||
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
|
||||
INPUT_TAG: ${{ github.event.inputs.tag }}
|
||||
run: |
|
||||
git config --global user.name "MervinPraison"
|
||||
git config --global user.email "[email protected]"
|
||||
|
||||
# Remove tag if it exists locally
|
||||
git tag -d "$INPUT_TAG" || true
|
||||
|
||||
# Remove tag if it exists remotely
|
||||
git push origin :refs/tags/"$INPUT_TAG" || true
|
||||
|
||||
- name: Fetch tag from upstream
|
||||
env:
|
||||
UPSTREAM_COMMIT: ${{ steps.get_upstream_commit.outputs.upstream_commit }}
|
||||
run: |
|
||||
git fetch upstream $UPSTREAM_COMMIT --force
|
||||
|
||||
- name: Reset to upstream commit
|
||||
env:
|
||||
UPSTREAM_COMMIT: ${{ steps.get_upstream_commit.outputs.upstream_commit }}
|
||||
run: |
|
||||
git reset --hard $UPSTREAM_COMMIT
|
||||
|
||||
# Create and push tag
|
||||
git tag -a "$INPUT_TAG" -m "Release $INPUT_TAG (synced from upstream MervinPraison/PraisonAI@$INPUT_TAG)"
|
||||
git push origin "$INPUT_TAG"
|
||||
|
||||
# Create the release
|
||||
- name: Create Release
|
||||
|
||||
@@ -67,9 +67,11 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Run Comprehensive Test Suite
|
||||
env:
|
||||
INPUT_TEST_TYPE: ${{ github.event.inputs.test_type || 'all' }}
|
||||
run: |
|
||||
# Determine test type from input or default to 'all'
|
||||
TEST_TYPE="${{ github.event.inputs.test_type || 'all' }}"
|
||||
TEST_TYPE="$INPUT_TEST_TYPE"
|
||||
|
||||
echo "🧪 Running comprehensive test suite (type: $TEST_TYPE)"
|
||||
|
||||
@@ -102,13 +104,16 @@ jobs:
|
||||
|
||||
- name: Generate Comprehensive Test Report
|
||||
if: always()
|
||||
env:
|
||||
INPUT_TEST_TYPE: ${{ github.event.inputs.test_type || 'all' }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
echo "# 📋 Comprehensive Test Report" > comprehensive_report.md
|
||||
echo "" >> comprehensive_report.md
|
||||
echo "**Python Version:** ${{ matrix.python-version }}" >> comprehensive_report.md
|
||||
echo "**Test Type:** ${{ github.event.inputs.test_type || 'all' }}" >> comprehensive_report.md
|
||||
echo "**Trigger:** ${{ github.event_name }}" >> comprehensive_report.md
|
||||
echo "**Date:** $(date -u)" >> comprehensive_report.md
|
||||
echo "**Test Type:** $INPUT_TEST_TYPE" >> comprehensive_report.md
|
||||
echo "**Trigger:** $EVENT_NAME" >> comprehensive_report.md
|
||||
echo "**Status:** 🟢 All Tests Passed" >> comprehensive_report.md
|
||||
echo "" >> comprehensive_report.md
|
||||
|
||||
echo "## 🧪 Test Categories Covered:" >> comprehensive_report.md
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -34,11 +34,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 🚨 Cost Warning
|
||||
env:
|
||||
INPUT_FRAMEWORK: ${{ github.event.inputs.framework }}
|
||||
INPUT_CONFIRM_COSTS: ${{ github.event.inputs.confirm_costs }}
|
||||
run: |
|
||||
echo "⚠️ WARNING: This workflow will make real API calls!"
|
||||
echo "💰 This may incur charges on your API accounts"
|
||||
echo "🎯 Framework: ${{ github.event.inputs.framework }}"
|
||||
echo "✅ Cost confirmation: ${{ github.event.inputs.confirm_costs }}"
|
||||
echo "🎯 Framework: $INPUT_FRAMEWORK"
|
||||
echo "✅ Cost confirmation: $INPUT_CONFIRM_COSTS"
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -93,21 +96,23 @@ jobs:
|
||||
|
||||
- name: Generate Real Test Report
|
||||
if: always()
|
||||
env:
|
||||
INPUT_FRAMEWORK: ${{ github.event.inputs.framework }}
|
||||
run: |
|
||||
echo "# 🔥 Real End-to-End Test Report" > real_test_report.md
|
||||
echo "" >> real_test_report.md
|
||||
echo "⚠️ **WARNING: This report represents tests that made real API calls**" >> real_test_report.md
|
||||
echo "" >> real_test_report.md
|
||||
echo "**Framework Tested:** ${{ github.event.inputs.framework }}" >> real_test_report.md
|
||||
echo "**Framework Tested:** $INPUT_FRAMEWORK" >> real_test_report.md
|
||||
echo "**Python Version:** ${{ matrix.python-version }}" >> real_test_report.md
|
||||
echo "**Date:** $(date -u)" >> real_test_report.md
|
||||
echo "**Triggered by:** ${{ github.actor }}" >> real_test_report.md
|
||||
echo "**Triggered by:** $GITHUB_ACTOR" >> real_test_report.md
|
||||
echo "" >> real_test_report.md
|
||||
|
||||
echo "## 🧪 Test Results" >> real_test_report.md
|
||||
echo "" >> real_test_report.md
|
||||
|
||||
if [ "${{ github.event.inputs.framework }}" == "autogen" ] || [ "${{ github.event.inputs.framework }}" == "all" ]; then
|
||||
if [ "$INPUT_FRAMEWORK" == "autogen" ] || [ "$INPUT_FRAMEWORK" == "all" ]; then
|
||||
echo "### AutoGen Real Tests:" >> real_test_report.md
|
||||
echo "- Environment verification" >> real_test_report.md
|
||||
echo "- Agent creation with real API calls" >> real_test_report.md
|
||||
@@ -115,7 +120,7 @@ jobs:
|
||||
echo "" >> real_test_report.md
|
||||
fi
|
||||
|
||||
if [ "${{ github.event.inputs.framework }}" == "crewai" ] || [ "${{ github.event.inputs.framework }}" == "all" ]; then
|
||||
if [ "$INPUT_FRAMEWORK" == "crewai" ] || [ "$INPUT_FRAMEWORK" == "all" ]; then
|
||||
echo "### CrewAI Real Tests:" >> real_test_report.md
|
||||
echo "- Environment verification" >> real_test_report.md
|
||||
echo "- Crew creation with real API calls" >> real_test_report.md
|
||||
@@ -150,11 +155,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 🛡️ Safety Check Failed
|
||||
env:
|
||||
INPUT_FRAMEWORK: ${{ github.event.inputs.framework }}
|
||||
INPUT_CONFIRM_COSTS: ${{ github.event.inputs.confirm_costs }}
|
||||
run: |
|
||||
echo "🚨 Real tests not executed due to safety checks:"
|
||||
echo ""
|
||||
echo "✅ Costs confirmed: ${{ github.event.inputs.confirm_costs }}"
|
||||
echo "✅ Framework selected: ${{ github.event.inputs.framework }}"
|
||||
echo "✅ Costs confirmed: $INPUT_CONFIRM_COSTS"
|
||||
echo "✅ Framework selected: $INPUT_FRAMEWORK"
|
||||
echo ""
|
||||
echo "To run real tests:"
|
||||
echo "1. Select a framework (autogen, crewai, or all)"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -23,12 +23,41 @@
|
||||
|
||||
</div>
|
||||
|
||||
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.
|
||||
PraisonAI 🦞 — **Hire a 24/7 AI Workforce.** Stop writing boilerplate and start shipping autonomous agents that research, plan, and execute tasks across your apps. From one agent to an entire organization, deployed in 5 lines of code.
|
||||
|
||||
<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>
|
||||
<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">
|
||||
@@ -39,25 +68,6 @@ PraisonAI 🦞 — Automate and solve complex challenges with AI agent teams tha
|
||||
|
||||
---
|
||||
|
||||
> **Quick Paths:**
|
||||
> - 🆕 **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)
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Performance
|
||||
|
||||
PraisonAI is built for speed, with agent instantiation in under 4μs. This reduces overhead, improves responsiveness, and helps multi-agent systems scale efficiently in real-world production workloads.
|
||||
|
||||
| Performance Metric | PraisonAI |
|
||||
|--------------------|-----------|
|
||||
| Avg Instantiation Time | **3.77 μs** |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Use Cases
|
||||
|
||||
AI agents solving real-world problems across industries:
|
||||
@@ -73,9 +83,44 @@ AI agents solving real-world problems across industries:
|
||||
|
||||
---
|
||||
|
||||
## Supported Providers
|
||||
## 🚀 Meet your first Agent (Under 1 Minute)
|
||||
|
||||
PraisonAI supports 100+ LLM providers through seamless integration:
|
||||
1. Install the lightweight core SDK:
|
||||
```bash
|
||||
pip install praisonaiagents
|
||||
export OPENAI_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
2. Run your first autonomous agent:
|
||||
```python
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Give your agent a goal, and watch it work.
|
||||
agent = Agent(instructions="You are a senior data analyst.")
|
||||
agent.start("Analyze the top 3 tech trends of 2026 and format as a markdown table.")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌌 The PraisonAI Ecosystem
|
||||
|
||||
Start simple with the core SDK, or expand to full visual builders and dashboards when you're ready.
|
||||
|
||||
* **Core SDK (`praisonaiagents`)**: For pure Python development. `pip install praisonaiagents`
|
||||
* 💻 **PraisonAI CLI (`praisonai`)**: For terminal-based developers. `pip install praisonai`
|
||||
* 🦞 **Claw Dashboard**: Connect agents directly to Telegram, Slack, or Discord. `pip install "praisonai[claw]"`
|
||||
* 🔗 **Flow Visual Builder**: Drag-and-drop workflow creation. `pip install "praisonai[flow]"`
|
||||
* 🤖 **PraisonAI UI**: Clean chat interface. `pip install "praisonai[ui]"`
|
||||
|
||||
### JavaScript SDK
|
||||
|
||||
```bash
|
||||
npm install praisonai
|
||||
```
|
||||
|
||||
## 🧠 Supported Providers & Features
|
||||
|
||||
Powered by 100+ LLMs (OpenAI, Anthropic, Gemini & local models).
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/OpenAI-412991?style=flat&logo=openai&logoColor=white" alt="OpenAI" />
|
||||
@@ -133,6 +178,14 @@ 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>
|
||||
|
||||
---
|
||||
|
||||
## 🌟 Why PraisonAI?
|
||||
@@ -173,55 +226,6 @@ PraisonAI supports 100+ LLM providers through seamless integration:
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
Get started with PraisonAI in under 1 minute:
|
||||
|
||||
```bash
|
||||
# Install
|
||||
pip install praisonaiagents
|
||||
|
||||
# Set API key
|
||||
export OPENAI_API_KEY=your_key_here
|
||||
|
||||
# Create a simple agent
|
||||
python -c "from praisonaiagents import Agent; Agent(instructions='You are a helpful AI assistant').start('Write a haiku about AI')"
|
||||
```
|
||||
|
||||
> **Next Steps:** [Single Agent Example](#1-single-agent) | [Multi Agents](#2-multi-agents) | [Full Docs](https://docs.praison.ai)
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Python SDK
|
||||
|
||||
Lightweight package dedicated for coding:
|
||||
|
||||
```bash
|
||||
pip install praisonaiagents
|
||||
```
|
||||
|
||||
For the full framework with CLI support:
|
||||
|
||||
```bash
|
||||
pip install praisonai
|
||||
```
|
||||
|
||||
🦞 **AgentClaw** — full UI with bots, memory, knowledge, and gateway:
|
||||
|
||||
```bash
|
||||
pip install "praisonai[claw]"
|
||||
praisonai claw
|
||||
```
|
||||
|
||||
### JavaScript SDK
|
||||
|
||||
```bash
|
||||
npm install praisonai
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -313,7 +317,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 +330,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
|
||||
@@ -623,6 +724,20 @@ const agent = new Agent({ instructions: 'You are a helpful AI assistant' });
|
||||
agent.start('Write a movie script about a robot in Mars');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Performance
|
||||
|
||||
PraisonAI is built for speed, with agent instantiation in under 4μs. This reduces overhead, improves responsiveness, and helps multi-agent systems scale efficiently in real-world production workloads.
|
||||
|
||||
| Performance Metric | PraisonAI |
|
||||
|--------------------|-----------|
|
||||
| Avg Instantiation Time | **3.77 μs** |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
@@ -28,39 +28,25 @@ 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">aclose</a>() -> None</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">close</a>() -> 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 +54,30 @@ 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">is_closed</a>() -> bool</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 +229,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 +730,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 +790,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,11 +851,14 @@ 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>
|
||||
* <code title="cli">praisonai gateway start <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway status <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai github triage <a href="./src/praisonai/praisonai/cli/commands/github.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks add <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks list <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks remove <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
@@ -883,6 +866,13 @@ Methods:
|
||||
* <code title="cli">praisonai knowledge index <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai knowledge list <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai knowledge search <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse config <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse connect <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse langfuse-start <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse status <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse stop <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse test <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai langfuse version <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
|
||||
* <code title="cli">praisonai loop help <a href="./src/praisonai/praisonai/cli/commands/loop.py">--help</a></code>
|
||||
* <code title="cli">praisonai loop loop-main <a href="./src/praisonai/praisonai/cli/commands/loop.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp logs <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
@@ -1042,12 +1032,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 +1185,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.80" \
|
||||
"praisonai>=4.5.136" \
|
||||
"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.80" \
|
||||
"praisonai>=4.5.136" \
|
||||
"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.80" \
|
||||
"praisonai>=4.5.136" \
|
||||
"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]}...")
|
||||
|
||||
@@ -43,14 +43,34 @@ app = FastAPI(
|
||||
redoc_url="/redoc"
|
||||
)
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
# Add CORS middleware with secure configuration
|
||||
cors_origins = os.getenv("API_CORS_ORIGINS", "").split(",")
|
||||
cors_origins = [origin.strip() for origin in cors_origins if origin.strip() and origin.strip() != "*"]
|
||||
|
||||
# Default secure origins if none specified
|
||||
if not cors_origins:
|
||||
# Secure defaults for different environments
|
||||
if os.getenv("ENVIRONMENT") == "production":
|
||||
# In production, require explicit configuration
|
||||
cors_origins = []
|
||||
else:
|
||||
# Development defaults - restrict to local origins
|
||||
cors_origins = [
|
||||
"http://localhost:3000", # Development frontend
|
||||
"http://localhost:8000", # Local development
|
||||
"http://127.0.0.1:3000", # Local development
|
||||
"http://127.0.0.1:8000", # Local development
|
||||
]
|
||||
|
||||
# Only add CORS middleware if origins are specified
|
||||
if cors_origins:
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allow_headers=["Authorization", "Content-Type", "Origin", "Accept"],
|
||||
)
|
||||
|
||||
# Create directories for storing reports
|
||||
REPORTS_DIR = Path("generated_reports")
|
||||
|
||||
@@ -14,14 +14,17 @@ import subprocess
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run_cli(cmd: str, cwd: str = None) -> tuple[int, str, str]:
|
||||
"""Run a CLI command and return (exit_code, stdout, stderr)."""
|
||||
# Use shell=False with shlex.split for safer execution
|
||||
args = shlex.split(cmd)
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
shell=True,
|
||||
args,
|
||||
shell=False, # Use shell=False for security
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=cwd,
|
||||
|
||||
@@ -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)
|
||||
@@ -308,7 +308,7 @@ def manage_state_versioning() -> Dict[str, Any]:
|
||||
versions[version_id] = {
|
||||
"config": current_config.copy(),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"hash": hashlib.md5(json.dumps(current_config, sort_keys=True).encode()).hexdigest()
|
||||
"hash": hashlib.sha256(json.dumps(current_config, sort_keys=True).encode()).hexdigest()
|
||||
}
|
||||
|
||||
# Make changes
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from praisonaiagents import Agent, Task, AgentTeam
|
||||
import subprocess
|
||||
import shlex
|
||||
import os
|
||||
|
||||
def run_terminal_command(command: str):
|
||||
@@ -7,7 +8,9 @@ def run_terminal_command(command: str):
|
||||
Run a terminal command and return its output.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
# Use shell=False with shlex.split for safer execution
|
||||
args = shlex.split(command)
|
||||
result = subprocess.run(args, shell=False, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
print(f"Command output: {result}")
|
||||
return {"stdout": result.stdout, "stderr": result.stderr}
|
||||
except subprocess.CalledProcessError as e:
|
||||
|
||||
@@ -2,6 +2,7 @@ from praisonaiagents import Agent, Task, AgentTeam, error_logs
|
||||
import json, os
|
||||
from e2b_code_interpreter import Sandbox
|
||||
import subprocess
|
||||
import shlex
|
||||
|
||||
def code_interpret(code: str):
|
||||
"""
|
||||
@@ -40,7 +41,9 @@ def run_terminal_command(command: str):
|
||||
Run a terminal command and return its output.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
# Use shell=False with shlex.split for safer execution
|
||||
args = shlex.split(command)
|
||||
result = subprocess.run(args, shell=False, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
print(f"Command output: {result}")
|
||||
return {"stdout": result.stdout, "stderr": result.stderr}
|
||||
except subprocess.CalledProcessError as e:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,9 +18,8 @@ workflow:
|
||||
planning: true
|
||||
reasoning: true
|
||||
verbose: true
|
||||
memory_config:
|
||||
provider: chroma
|
||||
persist: true
|
||||
memory:
|
||||
backend: chroma
|
||||
|
||||
# Variables for template substitution
|
||||
variables:
|
||||
@@ -34,14 +33,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 +51,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,236 @@
|
||||
# PraisonAI PR Reviewer Integration Guide
|
||||
|
||||
This guide provides step-by-step instructions for integrating PraisonAI as an automated PR reviewer in your GitHub CI/CD pipeline.
|
||||
|
||||
## Overview
|
||||
|
||||
PraisonAI PR Reviewer implements a **Zero-Code, Multi-Agent PR Review System** that deploys specialized agents to analyze pull requests from multiple perspectives:
|
||||
|
||||
- 🔐 **Security Reviewer**: Identifies vulnerabilities and security issues
|
||||
- ⚡ **Performance Reviewer**: Analyzes for bottlenecks and inefficiencies
|
||||
- 📋 **Maintainability Reviewer**: Evaluates code quality and best practices
|
||||
- 👨💼 **Lead Reviewer**: Synthesizes feedback and posts comprehensive reviews
|
||||
|
||||
## Architecture
|
||||
|
||||
This integration follows PraisonAI's **Agent-Centric** and **Protocol-Driven Core** design principles:
|
||||
|
||||
```
|
||||
GitHub PR → @praisonai trigger → Multi-Agent Workflow → Comprehensive Review
|
||||
```
|
||||
|
||||
The solution leverages:
|
||||
- **GitHub Actions** for CI/CD orchestration
|
||||
- **PraisonAI CLI** for agent execution
|
||||
- **YAML Configuration** for agent team definition
|
||||
- **GitHub CLI** for PR interaction
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Repository Setup**:
|
||||
- GitHub repository with Actions enabled
|
||||
- Required secrets configured (see [Secrets Configuration](#secrets-configuration))
|
||||
|
||||
2. **PraisonAI Installation**:
|
||||
- The workflow automatically installs PraisonAI via `pip install praisonai`
|
||||
- No additional dependencies required
|
||||
|
||||
3. **GitHub App/Token**:
|
||||
- GitHub App with required permissions OR
|
||||
- Personal Access Token with `repo` and `pull_requests` permissions
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### Step 1: Copy Agent Configuration
|
||||
|
||||
The agent configuration is already provided at:
|
||||
```
|
||||
.github/praisonai-reviewer.yaml
|
||||
```
|
||||
|
||||
This file defines the multi-agent team and their specific responsibilities.
|
||||
|
||||
### Step 2: Create GitHub Workflow
|
||||
|
||||
**IMPORTANT**: Due to GitHub App permissions, the workflow file must be manually created.
|
||||
|
||||
1. Copy the template from:
|
||||
```
|
||||
examples/yaml/praisonai-pr-review.yml.template
|
||||
```
|
||||
|
||||
2. Save it as:
|
||||
```
|
||||
.github/workflows/praisonai-pr-review.yml
|
||||
```
|
||||
|
||||
### Step 3: Configure Secrets
|
||||
|
||||
Add the following secrets to your repository (`Settings > Secrets and variables > Actions`):
|
||||
|
||||
| Secret | Description | Required |
|
||||
|--------|-------------|----------|
|
||||
| `PRAISONAI_APP_ID` | GitHub App ID | Yes (if using GitHub App) |
|
||||
| `PRAISONAI_APP_PRIVATE_KEY` | GitHub App private key | Yes (if using GitHub App) |
|
||||
| `OPENAI_API_KEY` | OpenAI API key for LLM access | Yes |
|
||||
|
||||
**Alternative**: Use `GH_TOKEN` instead of GitHub App if you prefer PAT authentication.
|
||||
|
||||
### Step 4: Update Review Chain (Optional)
|
||||
|
||||
The review chain documentation has been updated to include PraisonAI:
|
||||
```
|
||||
CodeRabbit/Qodo → Gemini/PraisonAI (parallel) → Copilot → Claude (final)
|
||||
```
|
||||
|
||||
This ensures PraisonAI integrates seamlessly with existing review workflows.
|
||||
|
||||
## Usage
|
||||
|
||||
### Manual Trigger
|
||||
|
||||
1. **Workflow Dispatch**:
|
||||
- Go to `Actions > PraisonAI PR Review > Run workflow`
|
||||
- Enter the PR number to review
|
||||
|
||||
### Automatic Trigger
|
||||
|
||||
1. **Comment Trigger**:
|
||||
- Comment `@praisonai` on any pull request
|
||||
- Only repository owners, members, and collaborators can trigger
|
||||
|
||||
2. **With Instructions**:
|
||||
- `@praisonai focus on security vulnerabilities`
|
||||
- `@praisonai check performance and memory usage`
|
||||
- `@praisonai review for maintainability issues`
|
||||
|
||||
## Expected Output
|
||||
|
||||
When triggered, PraisonAI will post a comprehensive review with this structure:
|
||||
|
||||
```markdown
|
||||
## 📋 Review Summary
|
||||
[Brief overview and assessment]
|
||||
|
||||
## 🔍 General Feedback
|
||||
[Overall patterns and observations]
|
||||
|
||||
## 🎯 Specific Feedback
|
||||
### 🔴 Critical
|
||||
[Security vulnerabilities, breaking changes, major bugs]
|
||||
|
||||
### 🟡 High
|
||||
[Performance issues, design flaws, significant bugs]
|
||||
|
||||
### 🟢 Medium
|
||||
[Code quality improvements, minor optimizations]
|
||||
|
||||
### 🔵 Low
|
||||
[Documentation, naming suggestions, minor refactoring]
|
||||
|
||||
## ✅ Highlights
|
||||
[Positive aspects worth mentioning]
|
||||
|
||||
---
|
||||
*Review completed by PraisonAI Multi-Agent Team*
|
||||
```
|
||||
|
||||
## Integration with Existing Workflows
|
||||
|
||||
PraisonAI integrates seamlessly with the existing review chain:
|
||||
|
||||
1. **Parallel Execution**: Runs alongside Gemini for faster reviews
|
||||
2. **No Conflicts**: Uses unique trigger (`@praisonai`) to avoid interference
|
||||
3. **Complementary Analysis**: Provides different perspectives from other tools
|
||||
4. **Chain Continuation**: Claude final review incorporates PraisonAI feedback
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Authentication Errors**:
|
||||
- Verify `PRAISONAI_APP_ID` and `PRAISONAI_APP_PRIVATE_KEY` secrets are correctly set
|
||||
- Ensure GitHub App has required permissions
|
||||
|
||||
2. **PraisonAI Installation Fails**:
|
||||
- Check if Python setup step completed successfully
|
||||
- Verify internet connectivity for pip installation
|
||||
|
||||
3. **Agent Execution Fails**:
|
||||
- Check `OPENAI_API_KEY` secret is valid
|
||||
- Verify agent configuration YAML syntax
|
||||
|
||||
4. **Permission Denied**:
|
||||
- Ensure triggering user has required repository permissions
|
||||
- Check workflow file permissions configuration
|
||||
|
||||
### Debug Steps
|
||||
|
||||
1. **Check Workflow Logs**:
|
||||
- Go to `Actions > PraisonAI PR Review`
|
||||
- Click on failed run to see detailed logs
|
||||
|
||||
2. **Validate Configuration**:
|
||||
- Ensure `.github/praisonai-reviewer.yaml` syntax is valid
|
||||
- Test agent configuration locally if possible
|
||||
|
||||
3. **Test Manual Trigger**:
|
||||
- Use workflow dispatch to isolate comment trigger issues
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Agent Teams
|
||||
|
||||
Modify `.github/praisonai-reviewer.yaml` to:
|
||||
- Add specialized agents (e.g., Architecture Reviewer)
|
||||
- Adjust agent responsibilities
|
||||
- Customize review output format
|
||||
|
||||
### Integration with External Tools
|
||||
|
||||
Extend agents to integrate with:
|
||||
- Code quality tools (SonarQube, CodeClimate)
|
||||
- Security scanners (Snyk, SAST tools)
|
||||
- Performance profilers
|
||||
|
||||
### Environment-Specific Reviews
|
||||
|
||||
Configure different agent teams for:
|
||||
- Backend vs Frontend changes
|
||||
- Different programming languages
|
||||
- Specific project domains
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Execution Time**: Typically 3-5 minutes for comprehensive review
|
||||
- **Rate Limits**: Respects GitHub API and OpenAI rate limits
|
||||
- **Cost**: Uses OpenAI API - monitor usage for cost control
|
||||
- **Parallel Execution**: Agents run concurrently for efficiency
|
||||
|
||||
## Security
|
||||
|
||||
- **Secret Handling**: All credentials stored securely in GitHub Secrets
|
||||
- **Permissions**: Minimal required permissions for workflow execution
|
||||
- **Code Access**: Review-only access, no code modification capabilities
|
||||
- **Audit Trail**: All reviews logged in GitHub Actions logs
|
||||
|
||||
## Contributing
|
||||
|
||||
To improve the PraisonAI PR Reviewer:
|
||||
|
||||
1. **Agent Enhancement**: Improve agent prompts and capabilities
|
||||
2. **Workflow Optimization**: Enhance GitHub Actions workflow
|
||||
3. **Documentation**: Update guides and troubleshooting info
|
||||
4. **Integration**: Add support for additional tools and platforms
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
1. Check this guide first
|
||||
2. Review GitHub Actions logs
|
||||
3. Open issue in PraisonAI repository
|
||||
4. Tag with `ci/cd` and `pr-review` labels
|
||||
|
||||
---
|
||||
|
||||
*Generated as part of PraisonAI CI/CD PR Reviewer Integration (Issue #1329)*
|
||||
@@ -0,0 +1,68 @@
|
||||
name: PraisonAI PR Reviewer
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'Pull Request Number'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '@praisonai') &&
|
||||
github.actor != 'github-actions[bot]' &&
|
||||
github.event.comment.user.type != 'Bot' &&
|
||||
(github.event.comment.author_association == 'OWNER' ||
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR'))
|
||||
|
||||
steps:
|
||||
- name: Determine checkout ref
|
||||
id: dest
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "issue_comment" ]; then
|
||||
echo "ref=refs/pull/${{ github.event.issue.number }}/head" >> "$GITHUB_OUTPUT"
|
||||
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "ref=refs/pull/${{ inputs.pr_number }}/head" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ref=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.dest.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.PRAISONAI_APP_ID }}
|
||||
private_key: ${{ secrets.PRAISONAI_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install PraisonAI
|
||||
run: pip install "praisonai[all]"
|
||||
|
||||
- name: Run PraisonAI PR Review
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
run: |
|
||||
praisonai workflow run .github/praisonai-reviewer.yaml
|
||||
@@ -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,58 @@
|
||||
---
|
||||
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) -->
|
||||
PraisonAI (@praisonai) ─── triggered via comment
|
||||
↓ (~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 OR PraisonAI 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. |
|
||||
| `praisonai-pr-review.yml` | `issue_comment`, `workflow_dispatch` | PraisonAI multi-agent PR review triggered by @praisonai mentions. |
|
||||
| `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.
|
||||
|
||||
## PraisonAI Integration
|
||||
|
||||
PraisonAI provides multi-agent PR review capabilities through the native PraisonAI agent framework. When triggered with `@praisonai`, it deploys a team of specialized agents:
|
||||
|
||||
- **Security Reviewer**: Analyzes for vulnerabilities, authentication issues, and unsafe code practices
|
||||
- **Performance Reviewer**: Identifies bottlenecks, inefficient algorithms, and resource usage issues
|
||||
- **Maintainability Reviewer**: Evaluates code quality, documentation, and adherence to best practices
|
||||
- **Lead Reviewer**: Synthesizes feedback and posts comprehensive review comments
|
||||
|
||||
The workflow uses the agent configuration at `.github/praisonai-reviewer.yaml` and leverages the `praisonai` CLI for execution.
|
||||
|
||||
## 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
|
||||
@@ -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'),
|
||||
@@ -168,20 +170,19 @@ _LAZY_IMPORTS = {
|
||||
'Handoff': ('praisonaiagents.agent.handoff', 'Handoff'),
|
||||
'handoff': ('praisonaiagents.agent.handoff', 'handoff'),
|
||||
'handoff_filters': ('praisonaiagents.agent.handoff', 'handoff_filters'),
|
||||
'parallel_handoffs': ('praisonaiagents.agent.handoff', 'parallel_handoffs'),
|
||||
'RECOMMENDED_PROMPT_PREFIX': ('praisonaiagents.agent.handoff', 'RECOMMENDED_PROMPT_PREFIX'),
|
||||
'prompt_with_handoff_instructions': ('praisonaiagents.agent.handoff', 'prompt_with_handoff_instructions'),
|
||||
'HandoffConfig': ('praisonaiagents.agent.handoff', 'HandoffConfig'),
|
||||
'HandoffResult': ('praisonaiagents.agent.handoff', 'HandoffResult'),
|
||||
'HandoffInputData': ('praisonaiagents.agent.handoff', 'HandoffInputData'),
|
||||
'ContextPolicy': ('praisonaiagents.agent.handoff', 'ContextPolicy'),
|
||||
'HandoffError': ('praisonaiagents.agent.handoff', 'HandoffError'),
|
||||
'HandoffCycleError': ('praisonaiagents.agent.handoff', 'HandoffCycleError'),
|
||||
'HandoffDepthError': ('praisonaiagents.agent.handoff', 'HandoffDepthError'),
|
||||
'HandoffTimeoutError': ('praisonaiagents.agent.handoff', 'HandoffTimeoutError'),
|
||||
'HandoffError': ('praisonaiagents.errors', 'HandoffError'),
|
||||
'HandoffCycleError': ('praisonaiagents.errors', 'HandoffCycleError'),
|
||||
'HandoffDepthError': ('praisonaiagents.errors', 'HandoffDepthError'),
|
||||
'HandoffTimeoutError': ('praisonaiagents.errors', '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 +204,17 @@ _LAZY_IMPORTS = {
|
||||
|
||||
# Agent classes
|
||||
'Agent': ('praisonaiagents.agent.agent', 'Agent'),
|
||||
'BudgetExceededError': ('praisonaiagents.errors', 'BudgetExceededError'),
|
||||
|
||||
# Error hierarchy - structured exception handling
|
||||
'PraisonAIError': ('praisonaiagents.errors', 'PraisonAIError'),
|
||||
'ToolExecutionError': ('praisonaiagents.errors', 'ToolExecutionError'),
|
||||
'LLMError': ('praisonaiagents.errors', 'LLMError'),
|
||||
'ValidationError': ('praisonaiagents.errors', 'ValidationError'),
|
||||
'NetworkError': ('praisonaiagents.errors', 'NetworkError'),
|
||||
'ErrorContextProtocol': ('praisonaiagents.errors', 'ErrorContextProtocol'),
|
||||
'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 +501,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 +567,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 +607,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':
|
||||
@@ -90,7 +110,7 @@ def __getattr__(name):
|
||||
|
||||
# Handoff - lightweight
|
||||
_handoff_names = {
|
||||
'Handoff', 'handoff', 'handoff_filters',
|
||||
'Handoff', 'handoff', 'handoff_filters', 'parallel_handoffs',
|
||||
'RECOMMENDED_PROMPT_PREFIX', 'prompt_with_handoff_instructions',
|
||||
'HandoffConfig', 'HandoffResult', 'HandoffInputData',
|
||||
'ContextPolicy', 'HandoffError', 'HandoffCycleError',
|
||||
@@ -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',
|
||||
@@ -165,6 +214,7 @@ __all__ = [
|
||||
'Handoff',
|
||||
'handoff',
|
||||
'handoff_filters',
|
||||
'parallel_handoffs',
|
||||
'RECOMMENDED_PROMPT_PREFIX',
|
||||
'prompt_with_handoff_instructions',
|
||||
'HandoffConfig',
|
||||
@@ -198,4 +248,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."""
|
||||
|
||||