Compare commits

...
77 Commits
Author SHA1 Message Date
Mervin Praison a8b1948fc0 feat(ag2): add AG2 framework backend integration (#1143)
feat(ag2): add AG2 framework backend integration
2026-03-26 17:48:37 +00:00
Faridun Mirzoev 56f815c1b1 fix(ag2): address Qodo review issues - YAML config resolution, OPENAI_API_BASE, patch create=True
- Add _resolve() helper to read LLM config from YAML top-level, per-role,
  config_list, and env vars in priority order (fixes Bedrock YAML config ignored)
- Include OPENAI_API_BASE env var in base_url resolution chain
- Add create=True to all patch("autogen.*") calls so tests pass without ag2 installed
2026-03-26 09:48:14 -07:00
Faridun Mirzoev 6b30977d9e feat(ag2): add AG2 framework backend integration
Integrates AG2 (community fork of AutoGen, PyPI package: ag2) as a new
framework option alongside praisonai, crewai, and autogen.

Changes:
- pyproject.toml: add [ag2] optional dependency extra (ag2>=0.11.0)
- agents_generator.py: AG2 detection via importlib.metadata + _run_ag2()
  with LLMConfig dict pattern, GroupChat orchestration, Bedrock support,
  ChatResult.summary extraction, and TERMINATE cleanup
- auto.py: AG2_AVAILABLE flag + validation in AutoGenerator.__init__
- .env.example: add AG2 and AWS Bedrock environment variable templates
- examples/ag2/: basic, multi-agent, and Bedrock YAML examples
- tests: 16 unit tests + 9 mock integration tests (25/25 passing)

Detection uses importlib.metadata.distribution('ag2') + LLMConfig check.
All changes are purely additive — existing code paths unaffected.

E2E tested against OpenAI gpt-4o-mini: single-agent and multi-agent
GroupChat flows both produce correct output.
2026-03-24 16:47:46 -04:00
Mervin Praison 4966ec1ae1 Merge pull request #658 from MervinPraison/develop
Enhance Claude workflow by adding environment variable for GitHub tok…
2025-06-16 08:26:16 +02:00
MervinPraison c31870a991 Enhance Claude workflow by adding environment variable for GitHub token and implementing package visibility check and Docker authentication tests for improved reliability and security. 2025-06-16 07:25:57 +01:00
Mervin Praison 67f4a89062 Merge pull request #657 from MervinPraison/develop
Add debugging steps to Claude workflow for environment verification a…
2025-06-16 08:24:07 +02:00
MervinPraison a898bbb7d1 Add debugging steps to Claude workflow for environment verification and Docker testing 2025-06-16 07:23:50 +01:00
Mervin Praison 107d33deb7 Merge pull request #656 from MervinPraison/develop
Update Claude Code action to restrict package permissions and remove …
2025-06-16 08:21:21 +02:00
MervinPraison 9d963e4eeb Update Claude Code action to restrict package permissions and remove Docker image build step for streamlined workflow. 2025-06-16 07:21:02 +01:00
Mervin Praison 56b296b8d6 Merge pull request #655 from MervinPraison/develop
Update Claude Code action to enable package writing and add Docker im…
2025-06-16 08:18:07 +02:00
MervinPraison fddf999688 Update Claude Code action to enable package writing and add Docker image build and push step for enhanced deployment capabilities. 2025-06-16 07:14:29 +01:00
Mervin Praison 6374aba783 Merge pull request #654 from MervinPraison/develop
Update Claude Code action to use GitHub token for authentication, enh…
2025-06-16 08:13:03 +02:00
MervinPraison 9408d699e8 Update Claude Code action to use GitHub token for authentication, enhancing security and consistency in the workflow. 2025-06-16 07:12:10 +01:00
Mervin Praison 6358671ab9 Merge pull request #652 from MervinPraison/develop
Enhance Claude Code action by adding environment variable verificatio…
2025-06-15 07:43:51 +02:00
MervinPraison f5bd55bb9a Enhance Claude Code action by adding environment variable verification and updating Docker setup for improved reliability 2025-06-15 06:43:34 +01:00
Mervin Praison a93c571f33 Merge pull request #651 from MervinPraison/develop
Update Claude Code action reference to use local action for improved …
2025-06-15 07:42:13 +02:00
MervinPraison 8ea0e4c831 Update Claude Code action reference to use local action for improved maintainability 2025-06-15 06:41:42 +01:00
Mervin Praison 8a1dc24794 Merge pull request #650 from MervinPraison/develop
Update Claude Code action reference to use quotes for consistency
2025-06-15 07:39:30 +02:00
MervinPraison b331229578 Update Claude Code action reference to use quotes for consistency 2025-06-15 06:39:12 +01:00
Mervin Praison 7af52735cf Merge pull request #649 from MervinPraison/develop
Add GitHub Container Registry login step and update Claude Code actio…
2025-06-15 07:37:42 +02:00
MervinPraison 2902fec42e Add GitHub Container Registry login step and update Claude Code action reference 2025-06-15 06:37:28 +01:00
Mervin Praison 64f752a8e0 Merge pull request #648 from MervinPraison/develop
Update Docker image reference in Claude Code action for consistency
2025-06-15 07:35:30 +02:00
MervinPraison d994fb5047 Update Docker image reference in Claude Code action for consistency 2025-06-15 06:34:52 +01:00
Mervin Praison 760fb9530b Merge pull request #647 from MervinPraison/develop
Refactor Claude Code integration to use local action instead of Docke…
2025-06-15 07:33:04 +02:00
MervinPraison de165975b0 Refactor Claude Code integration to use local action instead of Docker image for improved maintainability 2025-06-15 06:32:43 +01:00
Mervin Praison dfd74a2fe7 Merge pull request #646 from MervinPraison/develop
Develop
2025-06-15 07:29:30 +02:00
MervinPraison 9265ca08a2 Update Claude Code integration to use Docker image and add GitHub token for authentication 2025-06-15 06:28:57 +01:00
MervinPraison 665df753fd Remove CLAUDE_CODE_INTEGRATION_PRAISONAI_AGENTS.md as it is no longer needed for the integration of Claude Code with PraisonAI Agents. This deletion simplifies the documentation and reflects the current state of the project. 2025-06-15 05:55:55 +01:00
Mervin Praison 94d991f4ec Merge pull request #643 from MervinPraison/develop
Update PraisonAI and dependencies to version 2.2.33
2025-06-14 08:45:31 +02:00
MervinPraison 632cf68423 Update PraisonAI and dependencies to version 2.2.33
- Updated PraisonAI version from 2.2.32 to 2.2.33 across multiple Dockerfiles and relevant files.
- Adjusted dependency versions in pyproject.toml and other files for consistency.
- Enhanced README documentation to reflect the updated version.

This change integrates the latest features and improvements from PraisonAI while ensuring compatibility with existing code.
2025-06-14 07:45:15 +01:00
Mervin Praison d956c605f7 Merge pull request #635 from MervinPraison/claude/issue-634-20250610_115418
feat: Refactor Claude Code integration to use PraisonAI Agents
2025-06-14 08:43:35 +02:00
Mervin Praison 7e3de10f32 Rename test_claude_code_integration.py to src/praisonai/tests/unit/test_claude_code_integration.py 2025-06-14 07:41:20 +01:00
Mervin Praison 79883f1af2 Merge pull request #642 from MervinPraison/develop
Update PraisonAI and dependencies to version 2.2.32
2025-06-14 08:39:04 +02:00
MervinPraison a098eb0150 Update PraisonAI and dependencies to version 2.2.32
- Updated PraisonAI version from 2.2.31 to 2.2.32 across multiple Dockerfiles and relevant files.
- Adjusted dependency versions in pyproject.toml and other files for consistency.
- Enhanced README documentation to reflect the updated version.

This change integrates the latest features and improvements from PraisonAI while ensuring compatibility with existing code.
2025-06-14 07:38:45 +01:00
Mervin Praison b9ce3b4ccb Merge pull request #641 from MervinPraison/develop
Allow to configure reflection prompt
2025-06-14 08:36:28 +02:00
claude[bot]andMervin Praison 58b44280df feat: add self-reflection support with configurable prompt to achat method
- Implement complete self-reflection logic in achat method to match chat method
- Support configurable reflect_prompt parameter in async operations  
- Add reflection loop with min/max limits and error handling
- Update method docstring to reflect new self-reflection capability
- Ensure consistent behavior between chat and achat methods

Co-authored-by: Mervin Praison <[email protected]>
2025-06-14 06:26:49 +00:00
MervinPraison 4dba09d6b1 Merge remote-tracking branch 'leo-origin/feature/add-reflection-prompt' into test-reflection-prompt 2025-06-14 07:20:09 +01:00
Mervin Praison a538513bec Merge pull request #640 from MervinPraison/develop
Develop
2025-06-14 08:17:26 +02:00
MervinPraison 4fb79207cf Merge branch 'main' of https://github.com/MervinPraison/PraisonAI into develop 2025-06-14 07:16:44 +01:00
MervinPraison 0b444fae34 Merge branch 'develop' of https://github.com/MervinPraison/PraisonAI into develop 2025-06-14 07:16:26 +01:00
MervinPraison bd34e45bbc Update PraisonAI and dependencies to version 2.2.31
- Updated PraisonAI version from 2.2.30 to 2.2.31 across multiple Dockerfiles and the Ruby formula.
- Adjusted dependency versions in pyproject.toml and other relevant files for consistency.
- Enhanced README documentation to reflect the updated version.

This change ensures that the latest features and improvements from PraisonAI are integrated while maintaining compatibility with existing code.
2025-06-14 07:16:25 +01:00
Mervin Praison f4f051e57e Merge pull request #638 from krkead/patch-2
Update chainlit_ui.py to work with newer chainlit versions
2025-06-14 08:13:56 +02:00
Mervin Praison 3c2610cc3c Merge pull request #639 from krkead/patch-1
Update agents.py to work with newer chainlit versions
2025-06-14 08:13:44 +02:00
krkead 9788454134 Update chainlit_ui.py to work with newer chainlit versions
The previous payload modification was incorrect
2025-06-13 18:04:52 -03:00
krkead e618004e60 Update agents.py to work with newer chainlit versions 2025-06-13 18:02:35 -03:00
Mervin Praison 384803ba8e Merge pull request #637 from DhivyaBharathy-web/add-mistral-agent
Add Mistral v0.3 Conversational Agent – Conceptual Workflow
2025-06-12 06:01:21 +02:00
DhivyaBharathy-web d3bb366a51 Add Mistral 7B Instruction Agent notebook 2025-06-11 10:49:16 +00:00
DhivyaBharathy e579e84b52 Add files via upload 2025-06-11 16:13:42 +05:30
leovs09 61440403f5 add agent constructor params description 2025-06-11 00:48:51 +02:00
leovs09 b615782175 Allow to configure reflection prompt 2025-06-11 00:24:59 +02:00
claude[bot]andMervinPraison 35199979f0 feat: Refactor Claude Code integration to use PraisonAI Agents
- Replace litellm direct usage with praisonaiagents framework
- Implement Claude Code as a custom tool that agents can decide to use
- Add --claudecode CLI flag support: `praisonai code --claudecode`
- Create intelligent agent that chooses when to use Claude Code vs regular responses
- Maintain full backward compatibility with litellm fallback
- Add UI toggle switch for enabling/disabling Claude Code
- Implement git operations with automatic branch creation and PR URLs
- Add comprehensive error handling and streaming support
- Include test script and detailed documentation

Key improvements:
- Agent-driven decision making instead of manual detection logic
- Better tool coordination and flexibility
- Minimal code changes with maintained compatibility
- Enhanced user experience with intelligent tool selection

Co-Authored-By: MervinPraison <[email protected]>
2025-06-10 12:02:19 +00:00
DhivyaBharathy eeb77cfd20 Add files via upload 2025-06-10 16:15:43 +05:30
Mervin Praison 00d7994601 Merge pull request #633 from DhivyaBharathy-web/add-nlp-asr-agent
Add NLP + ASR Agent (Official BERT Large + Whisper Large V3)
2025-06-10 08:55:08 +01:00
DhivyaBharathy 10aa49addc Merge branch 'main' into add-nlp-asr-agent 2025-06-10 12:05:50 +05:30
DhivyaBharathy-web 79c9cf8bf9 Add NLP + ASR Agent (Official BERT Large + Whisper Large V3) notebook 2025-06-10 06:32:31 +00:00
Mervin Praison a9416fb7fc Merge pull request #631 from MervinPraison/claude/issue-630-20250609_145612
feat: add remote agent connectivity with Google ADK-like API
2025-06-09 20:43:47 +01:00
Mervin Praison 547b40ee72 Rename test_remote_agent.py to src/praisonai/tests/unit/test_remote_agent.py 2025-06-09 20:43:32 +01:00
claude[bot]andMervinPraison 4fc35d1b10 feat: add remote agent connectivity with Google ADK-like API
- Enhanced Session class to support remote agent URLs
- Added agent_url parameter for direct agent connectivity  
- Implemented HTTP client for remote agent communication
- Added connection testing and validation
- Provided send_message() alias for Google ADK compatibility
- Created comprehensive examples and documentation
- Maintained full backward compatibility
- Added proper error handling for remote operations

Fixes #630

Co-authored-by: MervinPraison <[email protected]>
2025-06-09 15:02:42 +00:00
Mervin Praison c72d74987b Merge pull request #625 from DhivyaBharathy-web/add-mistral7b-agent
Add mistral7b agent
2025-06-08 00:14:37 +01:00
DhivyaBharathy 430ec6571a Update MistralTechAgent.ipynb 2025-06-07 14:22:57 +05:30
DhivyaBharathy e88288773a Add files via upload 2025-06-07 14:15:47 +05:30
DhivyaBharathy-web 9af03e3fef Add Mistral-7B-Instruct-v0.1 Instruction Agent notebook 2025-06-07 08:43:22 +00:00
Mervin Praison 7a7560621b Merge pull request #624 from MervinPraison/develop
Develop
2025-06-07 01:27:46 +01:00
MervinPraison 531c58ac70 Update PraisonAI and dependencies to version 2.2.31 and praisonaiagents to version 0.0.104
- Updated PraisonAI version in Dockerfiles and Ruby formula.
- Adjusted dependency versions in pyproject.toml and uv.lock for consistency.
- Enhanced README to reflect the new versioning.

This change ensures compatibility with the latest features and improvements while maintaining existing functionality.
2025-06-07 01:27:29 +01:00
MervinPraison 274ade8154 Disable litellm telemetry across multiple modules for improved performance and consistency
- Added environment variable to disable litellm telemetry in __init__.py, llm.py, and memory.py.
- Ensured telemetry is disabled after importing litellm to prevent unnecessary data collection.

This change enhances control over telemetry features while maintaining existing functionality.
2025-06-07 00:24:28 +01:00
Mervin Praison 83f4f874e9 Merge pull request #622 from DhivyaBharathy-web/add-groq-Llama3-agent
Add Groq Llama3-8b-8192 Agent notebook
2025-06-06 23:38:03 +01:00
MervinPraison 99cd88dd30 Enhance telemetry support and update dependencies
- Added 'posthog>=4.0.0' to dependencies and optional 'telemetry' section in pyproject.toml.
- Updated blog_agent to use llm_config for improved clarity.
- Disabled memory in PraisonAIAgents instantiation for better control.
- Introduced telemetry support with lazy loading and fallback functions in __init__.py.

This update improves the integration of telemetry features while maintaining existing functionality.
2025-06-06 17:52:43 +01:00
DhivyaBharathy 1d22818662 Add files via upload 2025-06-06 15:28:14 +05:30
DhivyaBharathy-web 4f38e7250a Add Groq Llama3-8b-8192 Agent notebook 2025-06-06 09:52:36 +00:00
Mervin Praison b736567375 Merge pull request #618 from MervinPraison/revert-617-claude/issue-612-20250606_060924
Revert "fix: prevent duplicate Task and Response display when using litellm with memory"
2025-06-06 07:28:27 +01:00
Mervin Praison d014ea8d18 Revert "fix: prevent duplicate Task and Response display when using litellm with memory" 2025-06-06 07:28:13 +01:00
Mervin Praison fd6db3ccfa Merge pull request #617 from MervinPraison/claude/issue-612-20250606_060924
fix: prevent duplicate Task and Response display when using litellm with memory
2025-06-06 07:24:32 +01:00
claude[bot]andMervinPraison 1f2f26c5cf fix: prevent duplicate Task and Response display when using litellm with memory
- Add suppress_display parameter to LLM.get_response() method (default: False)
- Update all display_interaction calls in LLM class to respect suppress_display flag
- Agent class now passes suppress_display=True to prevent LLM duplicate displays
- Agent handles display centrally to ensure single display per interaction
- Maintains full backward compatibility while fixing issue #612

Co-authored-by: MervinPraison <[email protected]>
2025-06-06 06:16:39 +00:00
Mervin Praison 1965f296b0 Merge pull request #614 from MervinPraison/revert-613-claude/issue-612-20250605_213542
Revert "fix: prevent duplicate Task and Response display when using litellm with memory"
2025-06-06 06:43:34 +01:00
Mervin Praison cc7036784b Revert "fix: prevent duplicate Task and Response display when using litellm with memory" 2025-06-06 06:43:21 +01:00
Mervin Praison 826a68c33e Merge pull request #613 from MervinPraison/claude/issue-612-20250605_213542
fix: prevent duplicate Task and Response display when using litellm with memory
2025-06-06 06:41:16 +01:00
claude[bot]andMervinPraison 88ceba804b fix: prevent duplicate Task and Response display when using litellm with memory
- Add display_interaction parameter to LLM.get_response() method (default: True)
- Update all internal display_interaction calls in LLM class to respect new parameter  
- Modify agent calls to pass display_interaction=False when using custom LLM instances
- Maintains backward compatibility while fixing duplicate displays (fixes #612)

Co-authored-by: MervinPraison <[email protected]>
2025-06-05 21:45:59 +00:00
79 changed files with 20279 additions and 179 deletions

No files matched your search

@@ -0,0 +1,9 @@
FROM node:20-alpine
RUN npm install -g @anthropic-ai/claude-code
WORKDIR /app
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]
@@ -0,0 +1,20 @@
name: "Claude Code Action"
description: "Custom Claude Code runner for PraisonAI"
inputs:
anthropic_api_key:
required: true
description: "Anthropic API key"
github_token:
required: true
description: "GitHub token for repo access"
runs:
using: "docker"
image: "ghcr.io/mervinpraison/praisonai-claudecode:latest"
env:
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
GITHUB_TOKEN: ${{ inputs.github_token }}
args:
- "--anthropic-api-key=${{ inputs.anthropic_api_key }}"
- "--github-token=${{ inputs.github_token }}"
@@ -0,0 +1,21 @@
#!/bin/sh
set -e
echo "Running Claude Code in CI mode..."
# Extract GitHub context and create a smart prompt
PROMPT="Analyse the GitHub issue or PR context and generate a smart response based on the repository context."
# Set environment variables from arguments
export ANTHROPIC_API_KEY="${1#--anthropic-api-key=}"
export GITHUB_TOKEN="${2#--github-token=}"
# Verify environment variables
if [ -z "$ANTHROPIC_API_KEY" ] || [ -z "$GITHUB_TOKEN" ]; then
echo "Error: Required environment variables are not set"
exit 1
fi
# Run Claude with the prompt
claude -p "$PROMPT"
+117
View File
@@ -0,0 +1,117 @@
name: "Claude Code Action"
description: "Run Claude Code in GitHub Actions workflows"
inputs:
github_token:
description: "GitHub token with repo and issues permissions"
required: true
anthropic_api_key:
description: "Anthropic API key"
required: true
prompt:
description: "The prompt to send to Claude Code"
required: false
default: ""
prompt_file:
description: "Path to a file containing the prompt to send to Claude Code"
required: false
default: ""
allowed_tools:
description: "Comma-separated list of allowed tools for Claude Code to use"
required: false
default: ""
output_file:
description: "File to save Claude Code output to (optional)"
required: false
default: ""
timeout_minutes:
description: "Timeout in minutes for Claude Code execution"
required: false
default: "10"
install_github_mcp:
description: "Whether to install the GitHub MCP server"
required: false
default: "false"
runs:
using: "composite"
steps:
- name: Install Claude Code
shell: bash
run: npm install -g @anthropic-ai/claude-code
- name: Install GitHub MCP Server
if: inputs.install_github_mcp == 'true'
shell: bash
run: |
claude mcp add-json github '{
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server:sha-ff3036d"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ inputs.GITHUB_TOKEN }}"
}
}'
- name: Prepare Prompt File
shell: bash
id: prepare_prompt
run: |
if [ -z "${{ inputs.prompt }}" ] && [ -z "${{ inputs.prompt_file }}" ]; then
echo "::error::Neither 'prompt' nor 'prompt_file' was provided. At least one is required."
exit 1
fi
if [ ! -z "${{ inputs.prompt_file }}" ]; then
if [ ! -f "${{ inputs.prompt_file }}" ]; then
echo "::error::Prompt file '${{ inputs.prompt_file }}' does not exist."
exit 1
fi
PROMPT_PATH="${{ inputs.prompt_file }}"
else
mkdir -p /tmp/claude-action
PROMPT_PATH="/tmp/claude-action/prompt.txt"
echo "${{ inputs.prompt }}" > "$PROMPT_PATH"
fi
if [ ! -s "$PROMPT_PATH" ]; then
echo "::error::Prompt is empty. Please provide a non-empty prompt."
exit 1
fi
echo "PROMPT_PATH=$PROMPT_PATH" >> $GITHUB_ENV
- name: Run Claude Code
shell: bash
id: run_claude
run: |
timeout_seconds=$((${{ inputs.timeout_minutes }} * 60))
if [ -z "${{ inputs.output_file }}" ]; then
timeout $timeout_seconds claude \
-p \
--verbose \
--output-format stream-json \
"$(cat ${{ env.PROMPT_PATH }})" \
${{ inputs.allowed_tools != '' && format('--allowedTools "{0}"', inputs.allowed_tools) || '' }}
else
timeout $timeout_seconds claude \
-p \
--verbose \
--output-format stream-json \
"$(cat ${{ env.PROMPT_PATH }})" \
${{ inputs.allowed_tools != '' && format('--allowedTools "{0}"', inputs.allowed_tools) || '' }} | tee output.txt
jq -s '.' output.txt > output.json
jq -r '.[-1].result' output.json > "${{ inputs.output_file }}"
echo "Complete output saved to output.json, final response saved to ${{ inputs.output_file }}"
fi
env:
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
GITHUB_TOKEN: ${{ inputs.github_token }}
+39
View File
@@ -0,0 +1,39 @@
name: Build Claude Code Image
on:
workflow_dispatch:
inputs:
version:
description: 'Image version tag'
required: true
default: 'latest'
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./.github/actions/claude-code-action
push: true
tags: ghcr.io/mervinpraison/praisonai-claudecode:${{ inputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
+46 -1
View File
@@ -23,15 +23,60 @@ jobs:
pull-requests: read
issues: read
id-token: write
packages: read
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Debug Environment
run: |
echo "=== Environment Debug ==="
echo "GITHUB_ACTOR: $GITHUB_ACTOR"
echo "GITHUB_REPOSITORY: $GITHUB_REPOSITORY"
echo "GITHUB_REPOSITORY_OWNER: $GITHUB_REPOSITORY_OWNER"
echo "GITHUB_TOKEN exists: $([ -n "$GITHUB_TOKEN" ] && echo "yes" || echo "no")"
echo "GITHUB_TOKEN length: ${#GITHUB_TOKEN}"
- name: Check Package Visibility
run: |
echo "=== Checking Package Visibility ==="
curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/users/MervinPraison/packages/container/praisonai-claudecode" \
|| echo "Package API call failed"
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Test Docker Authentication
run: |
echo "=== Testing Docker Authentication ==="
echo "Checking ~/.docker/config.json"
cat ~/.docker/config.json | jq '.' || echo "No docker config found"
echo "=== Testing Registry Access ==="
curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://ghcr.io/v2/" || echo "Registry access failed"
- name: Test Docker Pull
run: |
echo "=== Testing Docker Pull ==="
echo "Attempting to pull image: ghcr.io/mervinpraison/praisonai-claudecode:latest"
docker pull ghcr.io/mervinpraison/praisonai-claudecode:latest || echo "Pull failed with exit code: $?"
echo "=== Checking Docker Images ==="
docker images | grep praisonai-claudecode || echo "No images found"
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@beta
uses: ./.github/actions/claude-code-action
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
flask \
"praisonai>=2.2.30" \
"praisonai>=2.2.33" \
"praisonai[api]" \
gunicorn \
markdown
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.2.30" \
"praisonai>=2.2.33" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.2.30" \
"praisonai>=2.2.33" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.2.30" \
"praisonai>=2.2.33" \
"praisonai[ui]" \
"praisonai[crewai]"
+2 -2
View File
@@ -121,7 +121,7 @@ healthcheck:
## 📦 Package Versions
All Docker images use consistent, up-to-date versions:
- PraisonAI: `>=2.2.30`
- PraisonAI: `>=2.2.33`
- PraisonAI Agents: `>=0.0.92`
- Python: `3.11-slim`
@@ -218,7 +218,7 @@ docker-compose up -d
### Version Pinning
To use specific versions, update the Dockerfile:
```dockerfile
RUN pip install "praisonai==2.2.30" "praisonaiagents==0.0.92"
RUN pip install "praisonai==2.2.33" "praisonaiagents==0.0.92"
```
## 🌐 Production Deployment
+30
View File
@@ -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.
+42
View File
@@ -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.
+54
View File
@@ -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.
@@ -0,0 +1,245 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "a9343007",
"metadata": {
"id": "a9343007"
},
"source": [
"# Groq Llama3-8b-8192 Agent\n",
"This agent uses Groq's `llama3-8b-8192` model to answer questions about Groq technology.\n",
"It demonstrates a structured approach with YAML configs, prompt templates, and result handling."
]
},
{
"cell_type": "markdown",
"source": [
"[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/DhivyaBharathy-web/PraisonAI/blob/main/examples/cookbooks/Groq_LPU_Powered_AI_Assistant.ipynb)\n"
],
"metadata": {
"id": "mBWaooyypsyD"
},
"id": "mBWaooyypsyD"
},
{
"cell_type": "markdown",
"id": "b36ff766",
"metadata": {
"id": "b36ff766"
},
"source": [
"## Dependencies\n",
"We'll install the required Groq Python SDK.\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "518e7a6a",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "518e7a6a",
"outputId": "98149592-19d0-472a-c31b-aae763fc8b07"
},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"\u001b[?25l \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m0.0/129.6 kB\u001b[0m \u001b[31m?\u001b[0m eta \u001b[36m-:--:--\u001b[0m\r\u001b[2K \u001b[91m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[91m╸\u001b[0m\u001b[90m━━\u001b[0m \u001b[32m122.9/129.6 kB\u001b[0m \u001b[31m5.3 MB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\r\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m129.6/129.6 kB\u001b[0m \u001b[31m2.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
"\u001b[?25h"
]
}
],
"source": [
"!pip install --quiet groq pyyaml"
]
},
{
"cell_type": "markdown",
"id": "d8569a5f",
"metadata": {
"id": "d8569a5f"
},
"source": [
"## Tools Setup\n",
"Initialize Groq client and helper functions for prompt generation.\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "b59b0d09",
"metadata": {
"id": "b59b0d09"
},
"outputs": [],
"source": [
"import os\n",
"import yaml\n",
"from groq import Groq\n",
"\n",
"# Set API key (replace with your own or use environment variables)\n",
"os.environ['GROQ_API_KEY'] = 'enter your key'\n",
"\n",
"# Initialize Groq client\n",
"client = Groq()\n",
"\n",
"def run_groq_chat(prompt_messages, model='llama3-8b-8192'):\n",
" response = client.chat.completions.create(\n",
" model=model,\n",
" messages=prompt_messages\n",
" )\n",
" return response.choices[0].message.content\n"
]
},
{
"cell_type": "markdown",
"id": "9302e8e6",
"metadata": {
"id": "9302e8e6"
},
"source": [
"## YAML Configuration\n",
"Define model and prompt templates via YAML.\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "238b8f7f",
"metadata": {
"id": "238b8f7f"
},
"outputs": [],
"source": [
"yaml_config = '''\n",
"model: llama3-8b-8192\n",
"prompt_template: |\n",
" You are an expert AI assistant knowledgeable about Groq's technology.\n",
" Provide a detailed answer to the user's question.\n",
" Then summarize the key points briefly.\n",
"\n",
" User question: \"{user_question}\"\n",
"'''\n",
"\n",
"config = yaml.safe_load(yaml_config)\n"
]
},
{
"cell_type": "markdown",
"id": "6d2ff6b1",
"metadata": {
"id": "6d2ff6b1"
},
"source": [
"## Prompt Construction\n",
"Fill in the prompt template with user input.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "2d42e686",
"metadata": {
"id": "2d42e686"
},
"outputs": [],
"source": [
"def build_prompt(user_question):\n",
" prompt_text = config['prompt_template'].format(user_question=user_question)\n",
" messages = [\n",
" {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n",
" {\"role\": \"user\", \"content\": prompt_text}\n",
" ]\n",
" return messages\n"
]
},
{
"cell_type": "markdown",
"id": "3c9448d3",
"metadata": {
"id": "3c9448d3"
},
"source": [
"## Main Logic\n",
"Run the agent on an example question.\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "c59933e4",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "c59933e4",
"outputId": "bad10728-fd20-4e0d-fded-c29fd987a9b7"
},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"Agent response:\n",
"Groq's Low-Precision Unified (LPU) technology is a proprietary architecture designed to accelerate artificial intelligence (AI) and machine learning (ML) workloads. LPU-powered models, also known as Groq Models, have several advantages over traditional Graphics Processing Units (GPUs) in specific applications:\n",
"\n",
"1. **Improved energy efficiency**: LPU is optimized for low power consumption, making it suitable for edge, mobile, and embedded devices where power constraints are common. This is particularly important for applications that require long-lived deployments, such as autonomous vehicles or IoT sensors.\n",
"2. **Enhanced accuracy**: LPU's customized precision and data type selection enable better representational precision for numeric computations, resulting in improved model accuracy. This is particularly beneficial for tasks that require high-fidelity calculations, such as medical imaging or natural language processing.\n",
"3. **Simplified software development**: LPU's unified architecture simplifies the development process for AI/ML developers. Groq Models provide a consistent programming model across different inference scenarios, allowing for easier model deployment and optimization.\n",
"4. **Increased throughput**: LPU's optimized arithmetic units and pipelined architecture enable higher Throughput per Watt (TPW) compared to traditional GPUs. This translates to faster processing times and higher compute density.\n",
"5. **Flexibility and scalability**: LPU-powered models can be deployed across various hardware platforms, from small, low-power devices to large data center clusters. This flexibility allows developers to choose the optimal deployment scenario for their specific use case.\n",
"6. **Customization and specialization**: LPU's architecture can be customized for specific workloads, allowing for optimized performance and power consumption. This customization potential enables developers to create highly specialized AI/ML hardware that matches their specific requirements.\n",
"\n",
"In summary, Groq's LPU-powered models offer significant advantages over traditional GPUs in terms of energy efficiency, accuracy, software development simplicity, throughput, flexibility, and customization.\n",
"\n",
"Key points:\n",
"\n",
"* Improved energy efficiency and suitability for edge, mobile, and embedded devices\n",
"* Enhanced accuracy for high-fidelity calculations\n",
"* Simplified software development with a unified programming model\n",
"* Increased throughput and compute density\n",
"* Flexibility and scalability across various hardware platforms\n",
"* Customization potential for specific workloads and applications\n"
]
}
],
"source": [
"user_question = \"What are the advantages of Groq's LPU-powered models compared to traditional GPUs?\"\n",
"prompt_messages = build_prompt(user_question)\n",
"agent_response = run_groq_chat(prompt_messages, model=config['model'])\n",
"print(\"Agent response:\")\n",
"print(agent_response)\n"
]
},
{
"cell_type": "markdown",
"id": "304a52dd",
"metadata": {
"id": "304a52dd"
},
"source": [
"## Output\n",
"The agent provides a detailed answer followed by a brief summary.\n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"language_info": {
"name": "python"
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,134 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "441900e0"
},
"source": [
"# 🧠 Mistral v0.3 (7B) Conversational Agent\n",
"A conceptual demo notebook to illustrate the workflow and output of the Mistral model, even if it doesn't run on Colab Free Tier."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "b4ba0b88"
},
"source": [
"## 📄 Description\n",
"This notebook showcases the typical flow of using Mistral v0.3 (7B) for conversational tasks. While the model is too large for Google Colab Free Tier, this version visually explains what the code would do.\n"
]
},
{
"cell_type": "markdown",
"source": [
"[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/DhivyaBharathy-web/PraisonAI/blob/main/examples/cookbooks/Mistral_v0_3_Conversational_Demo.ipynb)\n"
],
"metadata": {
"id": "yzXURvxIlIDD"
}
},
{
"cell_type": "markdown",
"source": [
"# 📦 Dependencies"
],
"metadata": {
"id": "IUQSTocQk7ml"
}
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "6f2afc57"
},
"outputs": [],
"source": [
"\n",
"!pip install transformers accelerate torch\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "9d5dad27"
},
"source": [
"## 🛠 Tools Used\n",
"- 🤗 Transformers: For loading the model and tokenizer.\n",
"- PyTorch: For model inference.\n",
"- Accelerate: To manage device placement (CPU/GPU).\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "cd936b40"
},
"source": [
"## 🧾 YAML Prompt Format (Conceptual)\n",
"```yaml\n",
"task: conversation\n",
"input: \"What's the capital of France?\"\n",
"history: []\n",
"temperature: 0.7\n",
"top_p: 0.9\n",
"```\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "8abaaa62"
},
"outputs": [],
"source": [
"# 🧠 Main Inference Logic (Illustrative)\n",
"from transformers import AutoModelForCausalLM, AutoTokenizer\n",
"import torch\n",
"\n",
"model_id = \"mistralai/Mistral-7B-Instruct-v0.3\"\n",
"tokenizer = AutoTokenizer.from_pretrained(model_id)\n",
"model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.float16, device_map=\"auto\")\n",
"\n",
"prompt = \"What's the capital of France?\"\n",
"inputs = tokenizer(prompt, return_tensors=\"pt\").to(model.device)\n",
"outputs = model.generate(**inputs, max_new_tokens=100)\n",
"response = tokenizer.decode(outputs[0], skip_special_tokens=True)\n",
"print(response)\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "def486c6"
},
"source": [
"## 📤 Output (Example)\n",
"> \"The capital of France is Paris.\"\n",
"\n",
"![output](https://upload.wikimedia.org/wikipedia/commons/e/e6/Paris_Night.jpg)\n",
"\n"
]
}
],
"metadata": {
"accelerator": "GPU",
"colab": {
"gpuType": "T4",
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+170
View File
@@ -0,0 +1,170 @@
# Remote Agent Connectivity
This directory contains examples demonstrating PraisonAI's remote agent connectivity feature, which allows you to connect to agents running on different servers, similar to Google ADK's agent connectivity pattern.
## Overview
The remote agent connectivity feature enables:
- **Direct agent-to-agent connectivity** across networks
- **Session-based communication** with remote agents
- **Google ADK-like API patterns** for familiar usage
- **Automatic connection management** and error handling
## Quick Start
### 1. Start a Remote Agent Server
```bash
# Start a simple remote agent server
python remote_agent_server.py
# Or start multiple agents on one server
python remote_agent_server.py --multiple
```
### 2. Connect from Client
```python
from praisonaiagents import Session
# Connect to remote agent (similar to Google ADK)
session = Session(agent_url="192.168.1.10:8000/agent")
# Send messages to the remote agent
response = session.chat("Hello from the client!")
print(response)
# Alternative method (Google ADK pattern)
response = session.send_message("What can you help me with?")
print(response)
```
## Examples
- **`remote_agent_server.py`** - Set up agents as remote servers
- **`remote_agent_example.py`** - Connect to and communicate with remote agents
## API Comparison
### Google ADK Pattern
```python
from adk.agent import Agent
from adk.session import Session
session = Session(agent_url="192.168.1.10:8000")
response = session.send_message("Hello!")
```
### PraisonAI Pattern
```python
from praisonaiagents import Session
session = Session(agent_url="192.168.1.10:8000/agent")
response = session.chat("Hello!")
# OR
response = session.send_message("Hello!") # Google ADK compatibility
```
## Key Features
### ✅ **Seamless Connectivity**
- Automatic protocol detection (http/https)
- Connection testing and validation
- Configurable timeouts
### ✅ **Error Handling**
- Clear error messages for connection issues
- Graceful handling of network problems
- Appropriate fallbacks for unavailable services
### ✅ **Backward Compatibility**
- Existing local agent functionality unchanged
- Session API works for both local and remote scenarios
- No breaking changes to current code
### ✅ **Flexible Configuration**
- Support for IP:port format (`192.168.1.10:8000`)
- Full URL format (`http://server.com:8000/agent`)
- Customizable endpoints and paths
## Usage Patterns
### Single Remote Agent
```python
# Connect to a specific remote agent
session = Session(agent_url="192.168.1.10:8000/agent")
response = session.chat("Hello from remote client!")
```
### Multiple Remote Agents
```python
# Connect to different specialized agents
research_session = Session(agent_url="192.168.1.10:8000/research")
coding_session = Session(agent_url="192.168.1.10:8000/coding")
research_response = research_session.chat("Find info about AI trends")
coding_response = coding_session.chat("Help me debug this Python code")
```
### Error Handling
```python
try:
session = Session(agent_url="192.168.1.10:8000/agent")
response = session.chat("Test message")
print(f"Response: {response}")
except ConnectionError as e:
print(f"Failed to connect: {e}")
```
## Network Configuration
### Server Setup
```python
# Create and launch agent server
agent = Agent(name="RemoteAgent", role="Assistant")
agent.launch(path="/agent", port=8000, host="0.0.0.0")
```
### Client Connection
```python
# Connect from any network location
session = Session(agent_url="<server-ip>:8000/agent")
```
## Limitations for Remote Sessions
When using remote agent sessions, certain local operations are not available:
- **Memory operations** (`session.memory.*`)
- **Knowledge operations** (`session.knowledge.*`)
- **Local agent creation** (`session.Agent(...)`)
- **State management** (`session.save_state/restore_state`)
This design keeps remote and local operations clearly separated.
## Testing
Run the examples to test the functionality:
```bash
# Terminal 1: Start server
python remote_agent_server.py
# Terminal 2: Test client (update IP as needed)
python remote_agent_example.py
```
## Integration with Existing Code
The remote connectivity feature integrates seamlessly with existing PraisonAI code:
```python
# Existing local code works unchanged
local_session = Session(session_id="local_chat")
local_agent = local_session.Agent(name="LocalAgent", role="Assistant")
# New remote connectivity
remote_session = Session(agent_url="remote-server:8000/agent")
remote_response = remote_session.chat("Hello from remote!")
```
This provides a smooth migration path for applications that need to scale across multiple servers.
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""
Remote Agent Connectivity Example
This example demonstrates how to connect to agents running on remote servers,
similar to Google ADK's agent connectivity pattern.
Usage:
1. First, start a remote agent server:
python remote_agent_server.py
2. Then run this client to connect to it:
python remote_agent_example.py
"""
from praisonaiagents import Session
import time
def example_remote_agent_connection():
"""
Example demonstrating remote agent connectivity similar to Google ADK.
"""
print("🌐 Remote Agent Connectivity Example")
print("=" * 50)
# Example 1: Connect to remote agent using IP:port format
try:
print("\n1. Connecting to remote agent at 192.168.1.10:8000...")
# Create a session with remote agent URL (similar to Google ADK)
session = Session(agent_url="192.168.1.10:8000/agent")
print(f"✅ Connected! Session: {session}")
# Send a message to the remote agent
response = session.chat("Hello from the client!")
print(f"📨 Sent: 'Hello from the client!'")
print(f"📨 Received: {response}")
# Send another message using the alias method (Google ADK pattern)
response = session.send_message("What can you help me with?")
print(f"📨 Sent: 'What can you help me with?'")
print(f"📨 Received: {response}")
except ConnectionError as e:
print(f"❌ Connection failed: {e}")
print("💡 Make sure the remote agent server is running at 192.168.1.10:8000")
# Example 2: Connect to localhost for testing
try:
print("\n2. Connecting to localhost for testing...")
# Connect to local server (for testing)
session = Session(agent_url="localhost:8000/agent")
print(f"✅ Connected! Session: {session}")
# Test the connection
response = session.chat("Test message")
print(f"📨 Response: {response}")
except ConnectionError as e:
print(f"❌ Connection failed: {e}")
print("💡 Start a local agent server first with: python remote_agent_server.py")
def example_error_handling():
"""
Example demonstrating error handling for remote sessions.
"""
print("\n\n🚨 Error Handling Examples")
print("=" * 50)
try:
# Create a remote session
session = Session(agent_url="localhost:8000/agent")
# These operations are not available for remote sessions
print("\n❌ Attempting to access memory (should fail)...")
session.memory.store_short_term("test")
except ConnectionError:
print("⚠️ Cannot connect to remote agent (expected if server not running)")
except ValueError as e:
print(f"✅ Correctly caught error: {e}")
try:
session = Session(agent_url="localhost:8000/agent")
print("\n❌ Attempting to create local agent (should fail)...")
agent = session.Agent(name="Test", role="Assistant")
except ConnectionError:
print("⚠️ Cannot connect to remote agent (expected if server not running)")
except ValueError as e:
print(f"✅ Correctly caught error: {e}")
def comparison_with_google_adk():
"""
Shows the comparison between Google ADK and PraisonAI patterns.
"""
print("\n\n🔄 Comparison with Google ADK")
print("=" * 50)
print("\n📋 Google ADK Pattern:")
print("""
from adk.agent import Agent
from adk.session import Session
# Connect to remote agent
session = Session(agent_url="192.168.1.10:8000")
response = session.send_message("Hello from client!")
""")
print("📋 PraisonAI Pattern:")
print("""
from praisonaiagents import Session
# Connect to remote agent
session = Session(agent_url="192.168.1.10:8000/agent")
response = session.chat("Hello from client!")
# OR
response = session.send_message("Hello from client!")
""")
print("✨ Key similarities:")
print("• Direct agent URL connectivity")
print("• Simple session-based API")
print("• Similar message sending patterns")
print("• Error handling for connection issues")
if __name__ == "__main__":
example_remote_agent_connection()
example_error_handling()
comparison_with_google_adk()
print("\n\n🎯 Summary")
print("=" * 50)
print("✅ Remote agent connectivity implemented")
print("✅ Google ADK-like API pattern supported")
print("✅ Backward compatibility maintained")
print("✅ Proper error handling for remote sessions")
print("\n💡 To test with a real server, run remote_agent_server.py first!")
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""
Remote Agent Server Example
This example demonstrates how to set up an agent as a remote server
that can be accessed by other clients using the Session(agent_url=...) pattern.
Usage:
python remote_agent_server.py
Then connect from another machine/process:
python remote_agent_example.py
"""
from praisonaiagents import Agent
import time
def setup_remote_agent_server():
"""
Set up an agent as a remote server accessible via HTTP API.
"""
print("🚀 Setting up Remote Agent Server")
print("=" * 50)
# Create an agent that will serve as the remote agent
agent = Agent(
name="RemoteAssistant",
role="Helpful Remote AI Assistant",
goal="Assist users with their queries via remote connections",
backstory="I am an AI assistant running on a remote server, ready to help clients connect and interact with me.",
llm="gpt-4o-mini", # or any other supported model
verbose=True
)
print(f"📝 Created agent: {agent}")
print(f"🔧 Agent details:")
print(f" • Name: {agent.name}")
print(f" • Role: {agent.role}")
print(f" • Goal: {agent.goal}")
# Launch the agent as an HTTP API server
print(f"\n🌐 Launching agent server...")
print(f"📡 Server will be accessible at:")
print(f" • Local: http://localhost:8000/agent")
print(f" • Network: http://<your-ip>:8000/agent")
print(f"\n🔗 Clients can connect using:")
print(f" session = Session(agent_url='<server-ip>:8000/agent')")
print(f"\n⏹️ Press Ctrl+C to stop the server")
print("=" * 50)
# This will start the HTTP server and block until interrupted
try:
agent.launch(path="/agent", port=8000, host="0.0.0.0")
except KeyboardInterrupt:
print("\n\n🛑 Server stopped by user")
except Exception as e:
print(f"\n❌ Server error: {e}")
def setup_multiple_agents():
"""
Example of setting up multiple agents on the same server.
"""
print("🚀 Setting up Multiple Remote Agents")
print("=" * 50)
# Create different specialized agents
research_agent = Agent(
name="ResearchAgent",
role="Research Specialist",
instructions="You are a research specialist who helps with finding and analyzing information."
)
coding_agent = Agent(
name="CodingAgent",
role="Programming Assistant",
instructions="You are a programming assistant who helps with coding questions and debugging."
)
# Launch them on different endpoints
print("🌐 Launching multiple agents:")
print("📡 Research Agent: http://localhost:8000/research")
print("📡 Coding Agent: http://localhost:8000/coding")
print("\n🔗 Connect using:")
print(" research_session = Session(agent_url='localhost:8000/research')")
print(" coding_session = Session(agent_url='localhost:8000/coding')")
try:
# Launch both agents (this will start the server)
research_agent.launch(path="/research", port=8000, host="0.0.0.0")
coding_agent.launch(path="/coding", port=8000, host="0.0.0.0")
except KeyboardInterrupt:
print("\n\n🛑 Server stopped by user")
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "--multiple":
setup_multiple_agents()
else:
setup_remote_agent_server()
print("\n\n✅ Remote agent server example completed!")
print("💡 Tip: Use --multiple flag to run multiple agents example")
+33
View File
@@ -0,0 +1,33 @@
# Telemetry Implementation Summary
## What Was Fixed
1. **PostHog initialization error** - Removed invalid `events_to_ignore` parameter
2. **Missing imports** - Added `MinimalTelemetry` and `TelemetryCollector` imports to telemetry `__init__.py`
3. **Wrong method instrumentation** - Changed from `agent.execute()` to `agent.chat()`
4. **Task tracking** - Added instrumentation for `workflow.execute_task()`
5. **Automatic setup** - Added `auto_instrument_all()` in main `__init__.py`
6. **Automatic flush** - Added `atexit` handler to send data on program exit
## Current Status
✅ **Telemetry is now working automatically!**
- Enabled by default (opt-out via environment variables)
- Tracks agent executions and task completions
- Sends anonymous data to PostHog on program exit
- No manual setup required
## Metrics Example
```
Telemetry metrics collected:
- Agent executions: 4
- Task completions: 2
- Errors: 0
- Session ID: 33873e62396d8b4c
```
## To Disable
Set any of these environment variables:
- `PRAISONAI_TELEMETRY_DISABLED=true`
- `DO_NOT_TRACK=true`
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""
Debug auto-instrumentation.
"""
print("1. Import telemetry module...")
import praisonaiagents.telemetry
print(" Telemetry module imported")
print("\n2. Check if auto_instrument_all was called...")
print(f" _initialized: {praisonaiagents.telemetry._initialized}")
print("\n3. Import Agent and PraisonAIAgents...")
from praisonaiagents import Agent, PraisonAIAgents
print(" Classes imported")
print("\n4. Check if classes are instrumented...")
agent = Agent(name="Test", role="Test", goal="Test", instructions="Test")
print(f" Agent.__init__ name: {Agent.__init__.__name__}")
print(f" agent.execute exists: {hasattr(agent, 'execute')}")
print("\n5. Manually call auto_instrument_all...")
from praisonaiagents.telemetry.integration import auto_instrument_all
auto_instrument_all()
print(" auto_instrument_all() called")
print("\n6. Create new agent after instrumentation...")
agent2 = Agent(name="Test2", role="Test2", goal="Test2", instructions="Test2")
print(f" Agent.__init__ name after: {Agent.__init__.__name__}")
print(f" agent2.execute exists: {hasattr(agent2, 'execute')}")
print("\n7. Check if execute is wrapped...")
if hasattr(agent2, 'execute'):
print(f" agent2.execute name: {agent2.execute.__name__}")
print(f" agent2.execute wrapped: {hasattr(agent2.execute, '__wrapped__')}")
print("\n8. Import telemetry and check if it's working...")
from praisonaiagents.telemetry import get_telemetry
telemetry = get_telemetry()
print(f" Telemetry enabled: {telemetry.enabled}")
print(f" PostHog available: {telemetry._posthog is not None}")
# The key insight: auto_instrument_all needs to be called AFTER
# the Agent and PraisonAIAgents classes are imported!
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""
Debug telemetry instrumentation to see what's happening.
"""
import os
# Make sure telemetry is enabled
if 'PRAISONAI_TELEMETRY_DISABLED' in os.environ:
del os.environ['PRAISONAI_TELEMETRY_DISABLED']
print("1. Importing modules...")
from praisonaiagents import Agent, Task, PraisonAIAgents
from praisonaiagents.telemetry import get_telemetry
print("\n2. Checking telemetry status...")
telemetry = get_telemetry()
print(f"Telemetry enabled: {telemetry.enabled}")
print(f"PostHog available: {telemetry._posthog is not None}")
print("\n3. Creating agent...")
agent = Agent(
name="TestAgent",
role="Test Role",
goal="Test Goal",
instructions="Test instructions"
)
# Check if agent.execute is instrumented
print(f"\n4. Checking agent instrumentation...")
print(f"Agent has execute method: {hasattr(agent, 'execute')}")
if hasattr(agent, 'execute'):
print(f"Execute method type: {type(agent.execute)}")
print(f"Execute method name: {agent.execute.__name__ if hasattr(agent.execute, '__name__') else 'No name'}")
print(f"Is wrapped: {'instrumented' in str(agent.execute.__name__) if hasattr(agent.execute, '__name__') else 'Unknown'}")
print("\n5. Creating task...")
task = Task(
description="Test task",
expected_output="Test output",
agent=agent
)
print("\n6. Creating workflow...")
workflow = PraisonAIAgents(
agents=[agent],
tasks=[task],
process="sequential"
)
# Check if workflow.start is instrumented
print(f"\n7. Checking workflow instrumentation...")
print(f"Workflow has start method: {hasattr(workflow, 'start')}")
if hasattr(workflow, 'start'):
print(f"Start method type: {type(workflow.start)}")
print(f"Start method name: {workflow.start.__name__ if hasattr(workflow.start, '__name__') else 'No name'}")
print(f"Is wrapped: {'instrumented' in str(workflow.start.__name__) if hasattr(workflow.start, '__name__') else 'Unknown'}")
print("\n8. Running workflow...")
result = workflow.start()
print("\n9. Checking metrics...")
metrics = telemetry.get_metrics()
print(f"Metrics: {metrics}")
print("\n10. Manually tracking to verify telemetry works...")
telemetry.track_agent_execution("ManualTest", success=True)
telemetry.track_task_completion("ManualTask", success=True)
manual_metrics = telemetry.get_metrics()
print(f"After manual tracking: {manual_metrics['metrics']}")
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""
Debug double-counting in telemetry.
"""
from praisonaiagents import Agent, Task, PraisonAIAgents
from praisonaiagents.telemetry import get_telemetry
# Get telemetry instance
telemetry = get_telemetry()
# Clear any existing metrics by flushing
telemetry.flush()
print("Starting fresh telemetry tracking...\n")
# Create ONE agent
print("Creating 1 agent...")
agent = Agent(
name="SingleAgent",
role="Test Role",
goal="Test Goal",
instructions="Test instructions"
)
# Create ONE task
print("Creating 1 task...")
task = Task(
description="Single test task",
expected_output="Test output",
agent=agent
)
# Create workflow with ONE agent and ONE task
print("Creating workflow with 1 agent and 1 task...")
workflow = PraisonAIAgents(
agents=[agent],
tasks=[task],
process="sequential"
)
# Check metrics before running
metrics_before = telemetry.get_metrics()
print(f"\nMetrics BEFORE running workflow:")
print(f" Agent executions: {metrics_before['metrics']['agent_executions']}")
print(f" Task completions: {metrics_before['metrics']['task_completions']}")
# Run the workflow
print("\nRunning workflow...")
result = workflow.start()
# Check metrics after running
metrics_after = telemetry.get_metrics()
print(f"\nMetrics AFTER running workflow:")
print(f" Agent executions: {metrics_after['metrics']['agent_executions']} (expected: 1)")
print(f" Task completions: {metrics_after['metrics']['task_completions']} (expected: 1)")
if metrics_after['metrics']['agent_executions'] > 1:
print("\n❌ ISSUE: Agent executions are being double-counted!")
print(" Possible causes:")
print(" - Agent method is being called multiple times")
print(" - Instrumentation is being applied twice")
print(" - Multiple tracking calls for same execution")
if metrics_after['metrics']['task_completions'] > 1:
print("\n❌ ISSUE: Task completions are being double-counted!")
print(" Possible causes:")
print(" - Task completion is tracked in multiple places")
print(" - Instrumentation is being applied twice")
@@ -30,9 +30,50 @@ from .main import (
async_display_callbacks,
)
# Telemetry support (lazy loaded)
try:
from .telemetry import (
get_telemetry,
enable_telemetry,
disable_telemetry,
MinimalTelemetry,
TelemetryCollector
)
_telemetry_available = True
except ImportError:
# Telemetry not available - provide stub functions
_telemetry_available = False
def get_telemetry():
return None
def enable_telemetry(*args, **kwargs):
import logging
logging.warning(
"Telemetry not available. Install with: pip install praisonaiagents[telemetry]"
)
return None
def disable_telemetry():
pass
MinimalTelemetry = None
TelemetryCollector = None
# Add Agents as an alias for PraisonAIAgents
Agents = PraisonAIAgents
# Apply telemetry auto-instrumentation after all imports
if _telemetry_available:
try:
# Only instrument if telemetry is enabled
_telemetry = get_telemetry()
if _telemetry and _telemetry.enabled:
from .telemetry.integration import auto_instrument_all
auto_instrument_all(_telemetry)
except Exception:
# Silently fail if there are any issues
pass
__all__ = [
'Agent',
'ImageAgent',
@@ -60,5 +101,10 @@ __all__ = [
'Chunking',
'MCP',
'GuardrailResult',
'LLMGuardrail'
'LLMGuardrail',
'get_telemetry',
'enable_telemetry',
'disable_telemetry',
'MinimalTelemetry',
'TelemetryCollector'
]
@@ -368,11 +368,97 @@ class Agent:
max_reflect: int = 3,
min_reflect: int = 1,
reflect_llm: Optional[str] = None,
reflect_prompt: Optional[str] = None,
user_id: Optional[str] = None,
reasoning_steps: bool = False,
guardrail: Optional[Union[Callable[['TaskOutput'], Tuple[bool, Any]], str]] = None,
max_guardrail_retries: int = 3
):
"""Initialize an Agent instance.
Args:
name (Optional[str], optional): Name of the agent used for identification and logging.
If None, defaults to "Agent". Defaults to None.
role (Optional[str], optional): Role or job title that defines the agent's expertise
and behavior patterns. Examples: "Data Analyst", "Content Writer". Defaults to None.
goal (Optional[str], optional): Primary objective or goal the agent aims to achieve.
Defines the agent's purpose and success criteria. Defaults to None.
backstory (Optional[str], optional): Background story or context that shapes the agent's
personality and decision-making approach. Defaults to None.
instructions (Optional[str], optional): Direct instructions that override role, goal,
and backstory when provided. Used for simple, task-specific agents. Defaults to None.
llm (Optional[Union[str, Any]], optional): Language model configuration. Can be a model
name string (e.g., "gpt-4o", "anthropic/claude-3-sonnet") or a configured LLM object.
Defaults to environment variable OPENAI_MODEL_NAME or "gpt-4o".
tools (Optional[List[Any]], optional): List of tools, functions, or capabilities
available to the agent for task execution. Can include callables, tool objects,
or MCP instances. Defaults to None.
function_calling_llm (Optional[Any], optional): Dedicated language model for function
calling operations. If None, uses the main llm parameter. Defaults to None.
max_iter (int, optional): Maximum number of iterations the agent can perform during
task execution to prevent infinite loops. Defaults to 20.
max_rpm (Optional[int], optional): Maximum requests per minute to rate limit API calls
and prevent quota exhaustion. If None, no rate limiting is applied. Defaults to None.
max_execution_time (Optional[int], optional): Maximum execution time in seconds for
agent operations before timeout. If None, no time limit is enforced. Defaults to None.
memory (Optional[Any], optional): Memory system for storing and retrieving information
across conversations. Requires memory dependencies to be installed. Defaults to None.
verbose (bool, optional): Enable detailed logging and status updates during agent
execution for debugging and monitoring. Defaults to True.
allow_delegation (bool, optional): Allow the agent to delegate tasks to other agents
or sub-processes when appropriate. Defaults to False.
step_callback (Optional[Any], optional): Callback function called after each step
of agent execution for custom monitoring or intervention. Defaults to None.
cache (bool, optional): Enable caching of responses and computations to improve
performance and reduce API costs. Defaults to True.
system_template (Optional[str], optional): Custom template for system prompts that
overrides the default system prompt generation. Defaults to None.
prompt_template (Optional[str], optional): Template for formatting user prompts
before sending to the language model. Defaults to None.
response_template (Optional[str], optional): Template for formatting agent responses
before returning to the user. Defaults to None.
allow_code_execution (Optional[bool], optional): Enable the agent to execute code
snippets during task completion. Use with caution for security. Defaults to False.
max_retry_limit (int, optional): Maximum number of retry attempts for failed operations
before giving up. Helps handle transient errors. Defaults to 2.
respect_context_window (bool, optional): Automatically manage context window size
to prevent token limit errors with large conversations. Defaults to True.
code_execution_mode (Literal["safe", "unsafe"], optional): Safety mode for code execution.
"safe" restricts dangerous operations, "unsafe" allows full code execution. Defaults to "safe".
embedder_config (Optional[Dict[str, Any]], optional): Configuration dictionary for
text embedding models used in knowledge retrieval and similarity search. Defaults to None.
knowledge (Optional[List[str]], optional): List of knowledge sources (file paths, URLs,
or text content) to be processed and made available to the agent. Defaults to None.
knowledge_config (Optional[Dict[str, Any]], optional): Configuration for knowledge
processing and retrieval system including chunking and indexing parameters. Defaults to None.
use_system_prompt (Optional[bool], optional): Whether to include system prompts in
conversations to establish agent behavior and context. Defaults to True.
markdown (bool, optional): Enable markdown formatting in agent responses for better
readability and structure. Defaults to True.
self_reflect (bool, optional): Enable self-reflection capabilities where the agent
evaluates and improves its own responses. Defaults to False.
max_reflect (int, optional): Maximum number of self-reflection iterations to prevent
excessive reflection loops. Defaults to 3.
min_reflect (int, optional): Minimum number of self-reflection iterations required
before accepting a response as satisfactory. Defaults to 1.
reflect_llm (Optional[str], optional): Dedicated language model for self-reflection
operations. If None, uses the main llm parameter. Defaults to None.
reflect_prompt (Optional[str], optional): Custom prompt template for self-reflection
that guides the agent's self-evaluation process. Defaults to None.
user_id (Optional[str], optional): Unique identifier for the user or session to
enable personalized responses and memory isolation. Defaults to "praison".
reasoning_steps (bool, optional): Enable step-by-step reasoning output to show the
agent's thought process during problem solving. Defaults to False.
guardrail (Optional[Union[Callable[['TaskOutput'], Tuple[bool, Any]], str]], optional):
Safety mechanism to validate agent outputs. Can be a validation function or
description string for LLM-based validation. Defaults to None.
max_guardrail_retries (int, optional): Maximum number of retry attempts when guardrail
validation fails before giving up. Defaults to 3.
Raises:
ValueError: If all of name, role, goal, backstory, and instructions are None.
ImportError: If memory or LLM features are requested but dependencies are not installed.
"""
# Add check at start if memory is requested
if memory is not None:
try:
@@ -470,6 +556,7 @@ class Agent:
self.markdown = markdown
self.max_reflect = max_reflect
self.min_reflect = min_reflect
self.reflect_prompt = reflect_prompt
# Use the same model selection logic for reflect_llm
self.reflect_llm = reflect_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o')
self.console = Console() # Create a single console instance for the agent
@@ -1230,7 +1317,7 @@ Your Goal: {self.goal}
reflection_prompt = f"""
Reflect on your previous response: '{response_text}'.
Identify any flaws, improvements, or actions.
{self.reflect_prompt if self.reflect_prompt else "Identify any flaws, improvements, or actions."}
Provide a "satisfactory" status ('yes' or 'no').
Output MUST be JSON with 'reflection' and 'satisfactory'.
"""
@@ -1328,7 +1415,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
return cleaned
async def achat(self, prompt: str, temperature=0.2, tools=None, output_json=None, output_pydantic=None, reasoning_steps=False):
"""Async version of chat method. TODO: Requires Syncing with chat method."""
"""Async version of chat method with self-reflection support."""
# Log all parameter values when in debug mode
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
param_info = {
@@ -1501,10 +1588,79 @@ Your Goal: {self.goal}
messages=messages,
temperature=temperature
)
response_text = response.choices[0].message.content
# Handle self-reflection if enabled
if self.self_reflect:
reflection_count = 0
while True:
reflection_prompt = f"""
Reflect on your previous response: '{response_text}'.
{self.reflect_prompt if self.reflect_prompt else "Identify any flaws, improvements, or actions."}
Provide a "satisfactory" status ('yes' or 'no').
Output MUST be JSON with 'reflection' and 'satisfactory'.
"""
# Add reflection prompt to messages
reflection_messages = messages + [
{"role": "assistant", "content": response_text},
{"role": "user", "content": reflection_prompt}
]
try:
reflection_response = await async_client.beta.chat.completions.parse(
model=self.reflect_llm if self.reflect_llm else self.llm,
messages=reflection_messages,
temperature=temperature,
response_format=ReflectionOutput
)
reflection_output = reflection_response.choices[0].message.parsed
if self.verbose:
display_self_reflection(f"Agent {self.name} self reflection (using {self.reflect_llm if self.reflect_llm else self.llm}): reflection='{reflection_output.reflection}' satisfactory='{reflection_output.satisfactory}'", console=self.console)
# Only consider satisfactory after minimum reflections
if reflection_output.satisfactory == "yes" and reflection_count >= self.min_reflect - 1:
if self.verbose:
display_self_reflection("Agent marked the response as satisfactory after meeting minimum reflections", console=self.console)
break
# Check if we've hit max reflections
if reflection_count >= self.max_reflect - 1:
if self.verbose:
display_self_reflection("Maximum reflection count reached, returning current response", console=self.console)
break
# Regenerate response based on reflection
regenerate_messages = reflection_messages + [
{"role": "assistant", "content": f"Self Reflection: {reflection_output.reflection} Satisfactory?: {reflection_output.satisfactory}"},
{"role": "user", "content": "Now regenerate your response using the reflection you made"}
]
new_response = await async_client.chat.completions.create(
model=self.llm,
messages=regenerate_messages,
temperature=temperature
)
response_text = new_response.choices[0].message.content
reflection_count += 1
except Exception as e:
if self.verbose:
display_error(f"Error in parsing self-reflection json {e}. Retrying", console=self.console)
logging.error("Reflection parsing failed.", exc_info=True)
reflection_count += 1
if reflection_count >= self.max_reflect:
break
continue
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
total_time = time.time() - start_time
logging.debug(f"Agent.achat completed in {total_time:.2f} seconds")
return response.choices[0].message.content
return response_text
except Exception as e:
display_error(f"Error in chat completion: {e}")
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
@@ -1,5 +1,9 @@
import logging
import warnings
import os
# Disable litellm telemetry before any imports
os.environ["LITELLM_TELEMETRY"] = "False"
# Suppress all relevant logs at module level
logging.getLogger("litellm").setLevel(logging.ERROR)
@@ -17,4 +21,11 @@ logging.basicConfig(level=logging.WARNING)
# Import after suppressing warnings
from .llm import LLM, LLMContextLengthExceededException
# Ensure telemetry is disabled after import as well
try:
import litellm
litellm.telemetry = False
except ImportError:
pass
__all__ = ["LLM", "LLMContextLengthExceededException"]
@@ -17,6 +17,9 @@ from ..main import (
from rich.console import Console
from rich.live import Live
# Disable litellm telemetry before any imports
os.environ["LITELLM_TELEMETRY"] = "False"
# TODO: Include in-build tool calling in LLM class
# TODO: Restructure so that duplicate calls are not made (Sync with agent.py)
class LLMContextLengthExceededException(Exception):
@@ -108,6 +111,9 @@ class LLM:
):
try:
import litellm
# Disable telemetry
litellm.telemetry = False
# Set litellm options globally
litellm.set_verbose = False
litellm.success_callback = []
@@ -6,6 +6,9 @@ import shutil
from typing import Any, Dict, List, Optional, Union, Literal
import logging
# Disable litellm telemetry before any imports
os.environ["LITELLM_TELEMETRY"] = "False"
# Set up logger
logger = logging.getLogger(__name__)
@@ -31,6 +34,7 @@ except ImportError:
try:
import litellm
litellm.telemetry = False # Disable telemetry
LITELLM_AVAILABLE = True
except ImportError:
LITELLM_AVAILABLE = False
+172 -31
View File
@@ -7,6 +7,9 @@ session API for developers building stateful agent applications.
import os
import uuid
import requests
import json
import time
from typing import Any, Dict, List, Optional
from .agent import Agent
from .memory import Memory
@@ -22,68 +25,90 @@ class Session:
- Memory operations (short-term, long-term, user-specific)
- Knowledge base operations
- Agent state management
- Remote agent connectivity
Example:
Examples:
# Local session with agent
session = Session(session_id="chat_123", user_id="user_456")
agent = session.Agent(name="Assistant", role="Helpful AI")
# Create stateful agent
agent = session.Agent(
name="Assistant",
role="Helpful AI",
memory=True
)
# Remote agent session (similar to Google ADK)
session = Session(agent_url="192.168.1.10:8000/agent")
response = session.chat("Hello from remote client!")
# Save session state
session.save_state({"conversation_topic": "AI research"})
# Restore state later
session.restore_state()
"""
def __init__(
self,
session_id: Optional[str] = None,
user_id: Optional[str] = None,
agent_url: Optional[str] = None,
memory_config: Optional[Dict[str, Any]] = None,
knowledge_config: Optional[Dict[str, Any]] = None
knowledge_config: Optional[Dict[str, Any]] = None,
timeout: int = 30
):
"""
Initialize a new session with optional persistence.
Initialize a new session with optional persistence or remote agent connectivity.
Args:
session_id: Unique session identifier. Auto-generated if None.
user_id: User identifier for user-specific memory operations.
agent_url: URL of remote agent for direct connectivity (e.g., "192.168.1.10:8000/agent")
memory_config: Configuration for memory system (defaults to RAG)
knowledge_config: Configuration for knowledge base system
knowledge_config: Configuration for knowledge base system
timeout: HTTP timeout for remote agent calls (default: 30 seconds)
"""
self.session_id = session_id or str(uuid.uuid4())[:8]
self.user_id = user_id or "default_user"
self.agent_url = agent_url
self.timeout = timeout
self.is_remote = agent_url is not None
# Initialize memory with sensible defaults
default_memory_config = {
"provider": "rag",
"use_embedding": True,
"rag_db_path": f".praison/sessions/{self.session_id}/chroma_db"
}
if memory_config:
default_memory_config.update(memory_config)
self.memory_config = default_memory_config
# Validate agent_url format
if self.is_remote:
if not self.agent_url.startswith(('http://', 'https://')):
# Assume http if no protocol specified
self.agent_url = f"http://{self.agent_url}"
# Test connectivity to remote agent
self._test_remote_connection()
# Initialize knowledge with session-specific config
default_knowledge_config = knowledge_config or {}
self.knowledge_config = default_knowledge_config
# Initialize memory with sensible defaults (only for local sessions)
if not self.is_remote:
default_memory_config = {
"provider": "rag",
"use_embedding": True,
"rag_db_path": f".praison/sessions/{self.session_id}/chroma_db"
}
if memory_config:
default_memory_config.update(memory_config)
self.memory_config = default_memory_config
# Create session directory
os.makedirs(f".praison/sessions/{self.session_id}", exist_ok=True)
# Initialize knowledge with session-specific config
default_knowledge_config = knowledge_config or {}
self.knowledge_config = default_knowledge_config
# Initialize components lazily
self._memory = None
self._knowledge = None
self._agents_instance = None
# Create session directory
os.makedirs(f".praison/sessions/{self.session_id}", exist_ok=True)
# Initialize components lazily
self._memory = None
self._knowledge = None
self._agents_instance = None
else:
# For remote sessions, disable local memory/knowledge
self.memory_config = {}
self.knowledge_config = {}
self._memory = None
self._knowledge = None
self._agents_instance = None
@property
def memory(self) -> Memory:
"""Lazy-loaded memory instance"""
if self.is_remote:
raise ValueError("Memory operations are not available for remote agent sessions")
if self._memory is None:
self._memory = Memory(config=self.memory_config)
return self._memory
@@ -91,6 +116,8 @@ class Session:
@property
def knowledge(self) -> Knowledge:
"""Lazy-loaded knowledge instance"""
if self.is_remote:
raise ValueError("Knowledge operations are not available for remote agent sessions")
if self._knowledge is None:
self._knowledge = Knowledge(config=self.knowledge_config)
return self._knowledge
@@ -119,7 +146,13 @@ class Session:
Returns:
Configured Agent instance
Raises:
ValueError: If this is a remote session (use chat() instead)
"""
if self.is_remote:
raise ValueError("Cannot create local agents in remote sessions. Use chat() to communicate with the remote agent.")
agent_kwargs = {
"name": name,
"role": role,
@@ -150,7 +183,12 @@ class Session:
Args:
state_data: Dictionary of state data to save
Raises:
ValueError: If this is a remote session
"""
if self.is_remote:
raise ValueError("State operations are not available for remote agent sessions")
state_text = f"Session state: {state_data}"
self.memory.store_short_term(
text=state_text,
@@ -168,7 +206,12 @@ class Session:
Returns:
Dictionary of restored state data
Raises:
ValueError: If this is a remote session
"""
if self.is_remote:
raise ValueError("State operations are not available for remote agent sessions")
# Use metadata-based search for better SQLite compatibility
results = self.memory.search_short_term(
query=f"type:session_state",
@@ -284,7 +327,105 @@ class Session:
max_items=max_items
)
def _test_remote_connection(self) -> None:
"""
Test connectivity to the remote agent.
Raises:
ConnectionError: If unable to connect to the remote agent
"""
try:
# Try a simple GET request to check if the server is responding
test_url = self.agent_url.rstrip('/') + '/health' if '/health' not in self.agent_url else self.agent_url
response = requests.get(test_url, timeout=self.timeout)
if response.status_code != 200:
# If health endpoint fails, try the main endpoint
response = requests.head(self.agent_url, timeout=self.timeout)
if response.status_code not in [200, 405]: # 405 = Method Not Allowed is OK
raise ConnectionError(f"Remote agent returned status code: {response.status_code}")
print(f"✅ Successfully connected to remote agent at {self.agent_url}")
except requests.exceptions.Timeout:
raise ConnectionError(f"Timeout connecting to remote agent at {self.agent_url}")
except requests.exceptions.ConnectionError:
raise ConnectionError(f"Failed to connect to remote agent at {self.agent_url}")
except Exception as e:
raise ConnectionError(f"Error connecting to remote agent: {str(e)}")
def chat(self, message: str, **kwargs) -> str:
"""
Send a message to the remote agent or handle local session.
Args:
message: The message to send to the agent
**kwargs: Additional parameters for the request
Returns:
The agent's response
Raises:
ValueError: If this is not a remote session
ConnectionError: If unable to communicate with remote agent
"""
if not self.is_remote:
raise ValueError("chat() method is only available for remote agent sessions. Use Agent.chat() for local agents.")
try:
# Prepare the request payload
payload = {
"query": message,
"session_id": self.session_id,
"user_id": self.user_id,
**kwargs
}
# Send POST request to the remote agent
response = requests.post(
self.agent_url,
json=payload,
headers={"Content-Type": "application/json"},
timeout=self.timeout
)
# Check if request was successful
response.raise_for_status()
# Parse the response
result = response.json()
# Extract the agent's response
if isinstance(result, dict):
return result.get("response", str(result))
else:
return str(result)
except requests.exceptions.Timeout:
raise ConnectionError(f"Timeout communicating with remote agent at {self.agent_url}")
except requests.exceptions.ConnectionError:
raise ConnectionError(f"Failed to communicate with remote agent at {self.agent_url}")
except requests.exceptions.HTTPError as e:
raise ConnectionError(f"HTTP error from remote agent: {e}")
except json.JSONDecodeError:
# If response is not JSON, return the raw text
return response.text
except Exception as e:
raise ConnectionError(f"Error communicating with remote agent: {str(e)}")
def send_message(self, message: str, **kwargs) -> str:
"""
Alias for chat() method to match Google ADK pattern.
Args:
message: The message to send to the agent
**kwargs: Additional parameters for the request
Returns:
The agent's response
"""
return self.chat(message, **kwargs)
def __str__(self) -> str:
if self.is_remote:
return f"Session(id='{self.session_id}', user='{self.user_id}', remote_agent='{self.agent_url}')"
return f"Session(id='{self.session_id}', user='{self.user_id}')"
def __repr__(self) -> str:
@@ -0,0 +1,108 @@
# PraisonAI Agents Telemetry
This module provides minimal, privacy-focused telemetry for PraisonAI Agents.
## Privacy Guarantees
- **No personal data is collected** - No prompts, responses, or user content
- **Anonymous metrics only** - Usage counts and feature adoption
- **Opt-out by default** - Respects standard privacy preferences
- **Transparent collection** - See exactly what's tracked below
## What We Collect
We collect only anonymous usage metrics:
- Number of agent executions
- Number of task completions
- Tool usage (names only, no arguments)
- Error types (no error messages)
- Framework version and OS type
- Anonymous session ID (regenerated each run)
## Disabling Telemetry
Telemetry can be disabled in three ways:
### 1. Environment Variables (Recommended)
Set any of these environment variables:
```bash
export PRAISONAI_TELEMETRY_DISABLED=true
export PRAISONAI_DISABLE_TELEMETRY=true
export DO_NOT_TRACK=true # Universal standard
```
### 2. Programmatically
```python
from praisonaiagents.telemetry import disable_telemetry
disable_telemetry()
```
### 3. At Runtime
```python
from praisonaiagents.telemetry import get_telemetry
telemetry = get_telemetry()
telemetry.enabled = False
```
## Usage
The telemetry module integrates automatically with PraisonAI Agents:
```python
from praisonaiagents import Agent, Task, PraisonAIAgents
# Telemetry is automatically enabled (unless disabled by environment)
agent = Agent(name="MyAgent", role="Assistant")
task = Task(description="Help user", agent=agent)
workflow = PraisonAIAgents(agents=[agent], tasks=[task])
result = workflow.start()
# Check telemetry metrics
from praisonaiagents.telemetry import get_telemetry
telemetry = get_telemetry()
print(telemetry.get_metrics())
```
## Implementation Details
The telemetry implementation is minimal and lightweight:
- No external dependencies required
- No network calls in current implementation
- Metrics stored in memory only
- Future versions may send to PostHog or similar privacy-focused services
## Backward Compatibility
The module maintains compatibility with the previous telemetry interface:
```python
from praisonaiagents.telemetry import TelemetryCollector
collector = TelemetryCollector()
collector.start()
with collector.trace_agent_execution("MyAgent"):
# Agent execution code
pass
collector.stop()
```
## Contributing
When contributing to telemetry:
1. Never collect personal data or user content
2. Always make new metrics opt-out
3. Document what's collected
4. Keep the implementation minimal
## Future Plans
- Integration with PostHog for anonymous analytics
- Aggregate usage statistics dashboard
- Opt-in detailed performance metrics
- Self-hosted telemetry endpoint option
@@ -0,0 +1,102 @@
"""
PraisonAI Agents Minimal Telemetry Module
This module provides anonymous usage tracking with privacy-first design.
Telemetry is opt-out and can be disabled via environment variables:
- PRAISONAI_TELEMETRY_DISABLED=true
- PRAISONAI_DISABLE_TELEMETRY=true
- DO_NOT_TRACK=true
No personal data, prompts, or responses are collected.
"""
import os
import atexit
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from .telemetry import MinimalTelemetry, TelemetryCollector
# Import the classes for real (not just type checking)
from .telemetry import MinimalTelemetry, TelemetryCollector
__all__ = [
'get_telemetry',
'enable_telemetry',
'disable_telemetry',
'MinimalTelemetry',
'TelemetryCollector', # For backward compatibility
]
def get_telemetry() -> 'MinimalTelemetry':
"""Get the global telemetry instance."""
from .telemetry import get_telemetry as _get_telemetry
return _get_telemetry()
def enable_telemetry():
"""Enable telemetry (if not disabled by environment)."""
from .telemetry import enable_telemetry as _enable_telemetry
_enable_telemetry()
def disable_telemetry():
"""Disable telemetry."""
from .telemetry import disable_telemetry as _disable_telemetry
_disable_telemetry()
# Auto-instrumentation and cleanup setup
_initialized = False
_atexit_registered = False
def _ensure_atexit():
"""Ensure atexit handler is registered."""
global _atexit_registered
if _atexit_registered:
return
# Check if telemetry should be disabled
telemetry_disabled = any([
os.environ.get('PRAISONAI_TELEMETRY_DISABLED', '').lower() in ('true', '1', 'yes'),
os.environ.get('PRAISONAI_DISABLE_TELEMETRY', '').lower() in ('true', '1', 'yes'),
os.environ.get('DO_NOT_TRACK', '').lower() in ('true', '1', 'yes'),
])
if not telemetry_disabled:
# Register atexit handler to flush telemetry on exit
atexit.register(lambda: get_telemetry().flush())
_atexit_registered = True
def _initialize_telemetry():
"""Initialize telemetry with auto-instrumentation and cleanup."""
global _initialized
if _initialized:
return
# Ensure atexit is registered
_ensure_atexit()
# Check if telemetry should be disabled
telemetry_disabled = any([
os.environ.get('PRAISONAI_TELEMETRY_DISABLED', '').lower() in ('true', '1', 'yes'),
os.environ.get('PRAISONAI_DISABLE_TELEMETRY', '').lower() in ('true', '1', 'yes'),
os.environ.get('DO_NOT_TRACK', '').lower() in ('true', '1', 'yes'),
])
if not telemetry_disabled:
try:
# Defer the actual instrumentation to avoid circular imports
# This will be called when get_telemetry() is first accessed
_initialized = True
except Exception:
# Silently fail if there are any issues
pass
# No need for lazy auto-instrumentation here since main __init__.py handles it
# Initialize atexit handler early
_ensure_atexit()
@@ -0,0 +1,242 @@
"""
Simplified integration module for adding telemetry to core PraisonAI components.
"""
from typing import Any, Optional, TYPE_CHECKING
from functools import wraps
import time
if TYPE_CHECKING:
from .telemetry import MinimalTelemetry
from ..agent.agent import Agent
from ..task.task import Task
from ..agents.agents import PraisonAIAgents
def instrument_agent(agent: 'Agent', telemetry: Optional['MinimalTelemetry'] = None):
"""
Instrument an Agent instance with minimal telemetry.
Args:
agent: The Agent instance to instrument
telemetry: Optional telemetry instance (uses global if not provided)
"""
if not telemetry:
from .telemetry import get_telemetry
telemetry = get_telemetry()
if not telemetry.enabled:
return agent
# Check if agent is already instrumented to avoid double-counting
if hasattr(agent, '_telemetry_instrumented'):
return agent
# Store original methods
original_chat = agent.chat if hasattr(agent, 'chat') else None
original_start = agent.start if hasattr(agent, 'start') else None
original_run = agent.run if hasattr(agent, 'run') else None
original_execute_tool = agent.execute_tool if hasattr(agent, 'execute_tool') else None
# Wrap chat method if it exists (this is the main method called by workflow)
if original_chat:
@wraps(original_chat)
def instrumented_chat(*args, **kwargs):
try:
result = original_chat(*args, **kwargs)
telemetry.track_agent_execution(agent.name, success=True)
return result
except Exception as e:
telemetry.track_agent_execution(agent.name, success=False)
telemetry.track_error(type(e).__name__)
raise
agent.chat = instrumented_chat
# Wrap start method if it exists
if original_start:
@wraps(original_start)
def instrumented_start(*args, **kwargs):
try:
result = original_start(*args, **kwargs)
telemetry.track_agent_execution(agent.name, success=True)
return result
except Exception as e:
telemetry.track_agent_execution(agent.name, success=False)
telemetry.track_error(type(e).__name__)
raise
agent.start = instrumented_start
# Wrap run method if it exists
if original_run:
@wraps(original_run)
def instrumented_run(*args, **kwargs):
try:
result = original_run(*args, **kwargs)
telemetry.track_agent_execution(agent.name, success=True)
return result
except Exception as e:
telemetry.track_agent_execution(agent.name, success=False)
telemetry.track_error(type(e).__name__)
raise
agent.run = instrumented_run
# Wrap execute_tool method
if original_execute_tool:
@wraps(original_execute_tool)
def instrumented_execute_tool(tool_name: str, *args, **kwargs):
try:
result = original_execute_tool(tool_name, *args, **kwargs)
telemetry.track_tool_usage(tool_name, success=True)
return result
except Exception as e:
telemetry.track_tool_usage(tool_name, success=False)
telemetry.track_error(type(e).__name__)
raise
agent.execute_tool = instrumented_execute_tool
# Mark agent as instrumented to avoid double instrumentation
agent._telemetry_instrumented = True
return agent
def instrument_workflow(workflow: 'PraisonAIAgents', telemetry: Optional['MinimalTelemetry'] = None):
"""
Instrument a PraisonAIAgents workflow with minimal telemetry.
Args:
workflow: The PraisonAIAgents instance to instrument
telemetry: Optional telemetry instance (uses global if not provided)
"""
if not telemetry:
from .telemetry import get_telemetry
telemetry = get_telemetry()
if not telemetry.enabled:
return workflow
# Check if workflow is already instrumented to avoid double-counting
if hasattr(workflow, '_telemetry_instrumented'):
return workflow
# Track feature usage
telemetry.track_feature_usage(f"workflow_{workflow.process}" if hasattr(workflow, 'process') else "workflow")
# Instrument all agents in the workflow
if hasattr(workflow, 'agents') and workflow.agents:
for agent in workflow.agents:
instrument_agent(agent, telemetry)
# Wrap the execute_task method to track task completions
if hasattr(workflow, 'execute_task'):
original_execute_task = workflow.execute_task
@wraps(original_execute_task)
def instrumented_execute_task(task_id, *args, **kwargs):
task = None
try:
# Get task info
if hasattr(workflow, 'tasks') and task_id < len(workflow.tasks):
task = workflow.tasks[task_id]
result = original_execute_task(task_id, *args, **kwargs)
# Track task completion
task_name = task.name if task and hasattr(task, 'name') else f"task_{task_id}"
telemetry.track_task_completion(task_name, success=True)
return result
except Exception as e:
telemetry.track_error(type(e).__name__)
if task:
task_name = task.name if hasattr(task, 'name') else f"task_{task_id}"
telemetry.track_task_completion(task_name, success=False)
raise
workflow.execute_task = instrumented_execute_task
# Wrap the start method
original_start = workflow.start
@wraps(original_start)
def instrumented_start(*args, **kwargs):
try:
result = original_start(*args, **kwargs)
# Don't double-track here since agent.chat already tracks execution
return result
except Exception as e:
telemetry.track_error(type(e).__name__)
raise
workflow.start = instrumented_start
# Also wrap astart if it exists (async version)
if hasattr(workflow, 'astart'):
original_astart = workflow.astart
@wraps(original_astart)
async def instrumented_astart(*args, **kwargs):
try:
result = await original_astart(*args, **kwargs)
# Don't double-track here since agent.chat already tracks execution
return result
except Exception as e:
telemetry.track_error(type(e).__name__)
raise
workflow.astart = instrumented_astart
# Mark workflow as instrumented to avoid double instrumentation
workflow._telemetry_instrumented = True
return workflow
# Auto-instrumentation helper
def auto_instrument_all(telemetry: Optional['MinimalTelemetry'] = None):
"""
Automatically instrument all new instances of Agent and PraisonAIAgents.
This should be called after enabling telemetry.
Args:
telemetry: Optional telemetry instance (uses global if not provided)
"""
if not telemetry:
from .telemetry import get_telemetry
telemetry = get_telemetry()
if not telemetry.enabled:
return
try:
# Import the classes
from ..agent.agent import Agent
from ..agents.agents import PraisonAIAgents
# Store original __init__ methods
original_agent_init = Agent.__init__
original_workflow_init = PraisonAIAgents.__init__
# Wrap Agent.__init__
@wraps(original_agent_init)
def agent_init_wrapper(self, *args, **kwargs):
original_agent_init(self, *args, **kwargs)
instrument_agent(self, telemetry)
# Wrap PraisonAIAgents.__init__
@wraps(original_workflow_init)
def workflow_init_wrapper(self, *args, **kwargs):
original_workflow_init(self, *args, **kwargs)
instrument_workflow(self, telemetry)
# Apply wrapped constructors
Agent.__init__ = agent_init_wrapper
PraisonAIAgents.__init__ = workflow_init_wrapper
except ImportError:
# Classes not available, skip auto-instrumentation
pass
@@ -0,0 +1,372 @@
"""
Minimal telemetry implementation for PraisonAI Agents.
This module provides anonymous usage tracking with privacy-first design.
All telemetry is opt-out via environment variables.
"""
import os
import time
import platform
import hashlib
from typing import Dict, Any, Optional
from datetime import datetime
import logging
# Try to import PostHog
try:
from posthog import Posthog
POSTHOG_AVAILABLE = True
except ImportError:
POSTHOG_AVAILABLE = False
# Check for opt-out environment variables
_TELEMETRY_DISABLED = any([
os.environ.get('PRAISONAI_TELEMETRY_DISABLED', '').lower() in ('true', '1', 'yes'),
os.environ.get('PRAISONAI_DISABLE_TELEMETRY', '').lower() in ('true', '1', 'yes'),
os.environ.get('DO_NOT_TRACK', '').lower() in ('true', '1', 'yes'),
])
class MinimalTelemetry:
"""
Minimal telemetry collector for anonymous usage tracking.
Privacy guarantees:
- No personal data is collected
- No prompts, responses, or user content is tracked
- Only anonymous metrics about feature usage
- Respects DO_NOT_TRACK standard
- Can be disabled via environment variables
"""
def __init__(self, enabled: bool = None):
"""
Initialize the minimal telemetry collector.
Args:
enabled: Override the environment-based enable/disable setting
"""
# Respect explicit enabled parameter, otherwise check environment
if enabled is not None:
self.enabled = enabled
else:
self.enabled = not _TELEMETRY_DISABLED
self.logger = logging.getLogger(__name__)
if not self.enabled:
self.logger.debug("Telemetry is disabled")
return
# Generate anonymous session ID (not user ID)
session_data = f"{datetime.now().isoformat()}-{os.getpid()}-{time.time()}"
self.session_id = hashlib.sha256(session_data.encode()).hexdigest()[:16]
# Basic metrics storage
self._metrics = {
"agent_executions": 0,
"task_completions": 0,
"tool_calls": 0,
"errors": 0,
}
# Collect basic environment info (anonymous)
self._environment = {
"python_version": platform.python_version(),
"os_type": platform.system(),
"framework_version": self._get_framework_version(),
}
self.logger.debug(f"Telemetry enabled with session {self.session_id}")
# Initialize PostHog if available
if POSTHOG_AVAILABLE:
try:
self._posthog = Posthog(
project_api_key='phc_skZpl3eFLQJ4iYjsERNMbCO6jfeSJi2vyZlPahKgxZ7',
host='https://eu.i.posthog.com',
disable_geoip=True,
on_error=lambda e: self.logger.debug(f"PostHog error: {e}"),
sync_mode=False # Use async mode to prevent blocking
)
except:
self._posthog = None
else:
self._posthog = None
def _get_framework_version(self) -> str:
"""Get the PraisonAI Agents version."""
try:
from .. import __version__
return __version__
except ImportError:
return "unknown"
def track_agent_execution(self, agent_name: str = None, success: bool = True):
"""
Track an agent execution event.
Args:
agent_name: Name of the agent (not logged, just for counting)
success: Whether the execution was successful
"""
if not self.enabled:
return
self._metrics["agent_executions"] += 1
# In a real implementation, this would send to a backend
# For now, just log at debug level
self.logger.debug(f"Agent execution tracked: success={success}")
def track_task_completion(self, task_name: str = None, success: bool = True):
"""
Track a task completion event.
Args:
task_name: Name of the task (not logged, just for counting)
success: Whether the task completed successfully
"""
if not self.enabled:
return
self._metrics["task_completions"] += 1
self.logger.debug(f"Task completion tracked: success={success}")
def track_tool_usage(self, tool_name: str, success: bool = True):
"""
Track tool usage event.
Args:
tool_name: Name of the tool being used
success: Whether the tool call was successful
"""
if not self.enabled:
return
self._metrics["tool_calls"] += 1
# Only track tool name, not arguments or results
self.logger.debug(f"Tool usage tracked: {tool_name}, success={success}")
def track_error(self, error_type: str = None):
"""
Track an error event.
Args:
error_type: Type of error (not the full message)
"""
if not self.enabled:
return
self._metrics["errors"] += 1
# Only track error type, not full error messages
self.logger.debug(f"Error tracked: type={error_type or 'unknown'}")
def track_feature_usage(self, feature_name: str):
"""
Track usage of a specific feature.
Args:
feature_name: Name of the feature being used
"""
if not self.enabled:
return
# Track which features are being used
self.logger.debug(f"Feature usage tracked: {feature_name}")
def get_metrics(self) -> Dict[str, Any]:
"""
Get current metrics summary.
Returns:
Dictionary of current metrics
"""
if not self.enabled:
return {"enabled": False}
return {
"enabled": True,
"session_id": self.session_id,
"metrics": self._metrics.copy(),
"environment": self._environment.copy(),
}
def flush(self):
"""
Flush any pending telemetry data.
In a real implementation, this would send data to a backend.
"""
if not self.enabled:
return
metrics = self.get_metrics()
self.logger.debug(f"Telemetry flush: {metrics}")
# Send to PostHog if available
if hasattr(self, '_posthog') and self._posthog:
try:
self._posthog.capture(
distinct_id='anonymous',
event='sdk_used',
properties={
'version': self._environment['framework_version'],
'os': platform.system(),
'$process_person_profile': False,
'$geoip_disable': True
}
)
# Don't flush here - let PostHog handle it asynchronously
except:
pass
# Reset counters
for key in self._metrics:
if isinstance(self._metrics[key], int):
self._metrics[key] = 0
def shutdown(self):
"""
Shutdown telemetry and ensure all events are sent.
"""
if not self.enabled:
return
# Final flush
self.flush()
# Shutdown PostHog if available
if hasattr(self, '_posthog') and self._posthog:
try:
# Force a synchronous flush before shutdown
self._posthog.flush()
self._posthog.shutdown()
except:
pass
# Global telemetry instance
_telemetry_instance = None
def get_telemetry() -> MinimalTelemetry:
"""
Get the global telemetry instance.
Returns:
The global MinimalTelemetry instance
"""
global _telemetry_instance
if _telemetry_instance is None:
_telemetry_instance = MinimalTelemetry()
return _telemetry_instance
def disable_telemetry():
"""Programmatically disable telemetry."""
global _telemetry_instance
if _telemetry_instance:
_telemetry_instance.enabled = False
else:
_telemetry_instance = MinimalTelemetry(enabled=False)
def enable_telemetry():
"""Programmatically enable telemetry (if not disabled by environment)."""
global _telemetry_instance
if not _TELEMETRY_DISABLED:
if _telemetry_instance:
_telemetry_instance.enabled = True
else:
_telemetry_instance = MinimalTelemetry(enabled=True)
# For backward compatibility with existing code
class TelemetryCollector:
"""Backward compatibility wrapper for the old TelemetryCollector interface."""
def __init__(self, backend: str = "minimal", service_name: str = "praisonai-agents", **kwargs):
self.telemetry = get_telemetry()
def start(self):
"""Start telemetry collection."""
# No-op for minimal implementation
pass
def stop(self):
"""Stop telemetry collection and flush data."""
self.telemetry.flush()
def trace_agent_execution(self, agent_name: str, **attributes):
"""Compatibility method for agent execution tracking."""
from contextlib import contextmanager
@contextmanager
def _trace():
try:
yield None
self.telemetry.track_agent_execution(agent_name, success=True)
except Exception:
self.telemetry.track_agent_execution(agent_name, success=False)
raise
return _trace()
def trace_task_execution(self, task_name: str, agent_name: str = None, **attributes):
"""Compatibility method for task execution tracking."""
from contextlib import contextmanager
@contextmanager
def _trace():
try:
yield None
self.telemetry.track_task_completion(task_name, success=True)
except Exception:
self.telemetry.track_task_completion(task_name, success=False)
raise
return _trace()
def trace_tool_call(self, tool_name: str, **attributes):
"""Compatibility method for tool call tracking."""
from contextlib import contextmanager
@contextmanager
def _trace():
try:
yield None
self.telemetry.track_tool_usage(tool_name, success=True)
except Exception:
self.telemetry.track_tool_usage(tool_name, success=False)
raise
return _trace()
def trace_llm_call(self, model: str = None, **attributes):
"""Compatibility method for LLM call tracking."""
from contextlib import contextmanager
@contextmanager
def _trace():
# We don't track LLM calls in minimal telemetry
yield None
return _trace()
def record_tokens(self, prompt_tokens: int, completion_tokens: int, model: str = None):
"""Compatibility method - we don't track token usage."""
pass
def record_cost(self, cost: float, model: str = None):
"""Compatibility method - we don't track costs."""
pass
def get_metrics(self) -> Dict[str, Any]:
"""Get current metrics."""
return self.telemetry.get_metrics()
+12 -5
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.0.102"
version = "0.0.106"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
requires-python = ">=3.10"
authors = [
@@ -14,7 +14,8 @@ dependencies = [
"pydantic",
"rich",
"openai",
"mcp>=1.6.0"
"mcp>=1.6.0",
"posthog>=3.0.0"
]
[project.optional-dependencies]
@@ -26,7 +27,7 @@ mcp = [
memory = [
"chromadb>=1.0.0",
"litellm>=1.50.0",
"litellm>=1.72.0",
]
knowledge = [
@@ -44,7 +45,7 @@ graph = [
# Add LLM dependencies
llm = [
"litellm>=1.50.0",
"litellm>=1.72.0",
"pydantic>=2.4.2"
]
@@ -54,6 +55,11 @@ api = [
"uvicorn>=0.34.0"
]
# Telemetry dependencies
telemetry = [
"posthog>=3.0.0"
]
# Combined features
all = [
"praisonaiagents[memory]",
@@ -61,7 +67,8 @@ all = [
"praisonaiagents[graph]",
"praisonaiagents[llm]",
"praisonaiagents[mcp]",
"praisonaiagents[api]"
"praisonaiagents[api]",
"praisonaiagents[telemetry]"
]
[tool.setuptools.packages.find]
+1
View File
@@ -2,6 +2,7 @@
pydantic
rich
openai>=1.30.0
posthog>=3.0.0
# Memory dependencies
chromadb>=0.5.23
+123
View File
@@ -0,0 +1,123 @@
# Telemetry Analysis: Why PostHog Events Aren't Being Sent by Default
## Executive Summary
The telemetry system is implemented but **not actively integrated** into the PraisonAI Agents codebase. While PostHog is properly configured and functional, telemetry data is never sent because:
1. **No automatic integration**: Agent and PraisonAIAgents classes don't use telemetry
2. **No automatic flush**: Events are collected but never sent to PostHog
3. **No lifecycle hooks**: No atexit handler or periodic flush mechanism
## Current State
### What's Working ✓
- PostHog client is properly initialized with API key and host
- Telemetry can be enabled/disabled via environment variables
- Privacy-first design with anonymous tracking
- Manual telemetry tracking and flushing works correctly
### What's Not Working ✗
- Agent class doesn't integrate telemetry
- PraisonAIAgents class doesn't integrate telemetry
- No automatic flush() calls anywhere in the codebase
- Integration module (integration.py) is not used
## Technical Analysis
### 1. PostHog Configuration (telemetry.py)
```python
# Lines 84-93: PostHog is initialized correctly
if POSTHOG_AVAILABLE:
try:
self._posthog = Posthog(
project_api_key='phc_skZpl3eFLQJ4iYjsERNMbCO6jfeSJi2vyZlPahKgxZ7',
host='https://eu.i.posthog.com'
)
except:
self._posthog = None
```
### 2. Flush Implementation (telemetry.py)
```python
# Lines 208-224: flush() sends events but is never called automatically
def flush(self):
if hasattr(self, '_posthog') and self._posthog:
try:
self._posthog.capture(
distinct_id='anonymous',
event='sdk_used',
properties={...}
)
self._posthog.capture('test-id', 'test-event')
except:
pass
```
### 3. Missing Integration Points
#### Agent Class (agent/agent.py)
- No telemetry imports
- No telemetry initialization in __init__
- No telemetry tracking in start(), run(), or execute_tool()
#### PraisonAIAgents Class (agents/agents.py)
- No telemetry imports
- No telemetry initialization
- No telemetry tracking in start() or workflow execution
## Root Causes
1. **Incomplete Implementation**: The telemetry system was built but never integrated into the main classes
2. **No Automatic Lifecycle Management**: No atexit handler or periodic flush
3. **Integration Module Not Used**: `integration.py` has the code but it's never imported/called
## Recommendations
### Immediate Fixes
1. **Add automatic integration to Agent class**:
```python
# In agent/agent.py __init__
from ..telemetry import get_telemetry
self._telemetry = get_telemetry()
# In start() method
if self._telemetry and self._telemetry.enabled:
self._telemetry.track_agent_execution(self.name, success=True)
```
2. **Add atexit handler in telemetry.py**:
```python
import atexit
def _flush_on_exit():
if _telemetry_instance:
_telemetry_instance.flush()
atexit.register(_flush_on_exit)
```
3. **Add periodic flush**:
```python
# Flush after N events or T seconds
if self._metrics["total_events"] >= 100:
self.flush()
```
### Long-term Improvements
1. **Make telemetry opt-in by default** with clear documentation
2. **Add telemetry configuration options** (flush interval, batch size)
3. **Implement proper error handling** for network failures
4. **Add telemetry dashboard** for users to see their usage
## Testing
Created test files demonstrate:
- `test_posthog.py`: PostHog is working correctly
- `test_posthog_detailed.py`: Events are sent when flush() is called
- `test_telemetry_integration.py`: Integration is missing
## Conclusion
The telemetry infrastructure is well-designed but incomplete. The main issue is that telemetry code exists in isolation and is never called by the core Agent/PraisonAIAgents classes. Additionally, even if it were integrated, the lack of automatic flush mechanisms means data would never reach PostHog.
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""
Example demonstrating the minimal telemetry implementation.
"""
import os
from praisonaiagents.telemetry import get_telemetry, disable_telemetry, enable_telemetry
# Example 1: Default telemetry (enabled unless disabled by environment)
print("=== Example 1: Default Telemetry ===")
telemetry = get_telemetry()
print(f"Telemetry enabled: {telemetry.enabled}")
# Track some events
telemetry.track_agent_execution("TestAgent", success=True)
telemetry.track_task_completion("TestTask", success=True)
telemetry.track_tool_usage("calculator", success=True)
telemetry.track_error("ValueError")
telemetry.track_feature_usage("memory")
# Get metrics
metrics = telemetry.get_metrics()
print(f"Current metrics: {metrics}")
# Example 2: Programmatically disable telemetry
print("\n=== Example 2: Disable Telemetry ===")
disable_telemetry()
telemetry = get_telemetry()
print(f"Telemetry enabled: {telemetry.enabled}")
# These won't be tracked
telemetry.track_agent_execution("TestAgent2", success=True)
metrics = telemetry.get_metrics()
print(f"Metrics after disable: {metrics}")
# Example 3: Re-enable telemetry
print("\n=== Example 3: Re-enable Telemetry ===")
enable_telemetry()
telemetry = get_telemetry()
print(f"Telemetry enabled: {telemetry.enabled}")
# Example 4: Test with environment variable
print("\n=== Example 4: Environment Variable Opt-out ===")
# Simulate environment variable being set
os.environ['PRAISONAI_TELEMETRY_DISABLED'] = 'true'
# Need to create a new instance to pick up the environment change
from importlib import reload
import praisonaiagents.telemetry.telemetry as telemetry_module
reload(telemetry_module)
from praisonaiagents.telemetry import get_telemetry as get_new_telemetry
new_telemetry = get_new_telemetry()
print(f"Telemetry enabled with env var: {new_telemetry.enabled}")
# Clean up
del os.environ['PRAISONAI_TELEMETRY_DISABLED']
# Example 5: Backward compatibility with TelemetryCollector
print("\n=== Example 5: Backward Compatibility ===")
from praisonaiagents.telemetry.telemetry import TelemetryCollector
collector = TelemetryCollector()
collector.start()
# Use context managers (backward compatible interface)
with collector.trace_agent_execution("CompatAgent"):
print("Executing agent...")
with collector.trace_tool_call("web_search"):
print("Calling tool...")
# Get metrics through collector
collector_metrics = collector.get_metrics()
print(f"Collector metrics: {collector_metrics}")
collector.stop()
print("\n=== Telemetry Example Complete ===")
print("\nPrivacy Notes:")
print("- No personal data, prompts, or responses are collected")
print("- Only anonymous usage metrics are tracked")
print("- Telemetry can be disabled via environment variables:")
print(" - PRAISONAI_TELEMETRY_DISABLED=true")
print(" - PRAISONAI_DISABLE_TELEMETRY=true")
print(" - DO_NOT_TRACK=true")
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""
Minimal example showing telemetry integration with agents.
"""
import os
# Uncomment to disable telemetry
# os.environ['PRAISONAI_TELEMETRY_DISABLED'] = 'true'
from praisonaiagents import Agent, Task, PraisonAIAgents
from praisonaiagents.telemetry import get_telemetry
# Create a simple agent
agent = Agent(
name="Calculator",
role="Math Expert",
goal="Perform calculations",
instructions="You are a helpful math expert."
)
# Create a task
task = Task(
description="Calculate 2 + 2",
expected_output="The sum",
agent=agent
)
# Create and run workflow
workflow = PraisonAIAgents(
agents=[agent],
tasks=[task],
process="sequential"
)
print("Running workflow with telemetry...")
result = workflow.start()
print(f"Result: {result}")
# Check telemetry metrics
telemetry = get_telemetry()
if telemetry.enabled:
metrics = telemetry.get_metrics()
print(f"\nTelemetry metrics collected:")
print(f"- Agent executions: {metrics['metrics']['agent_executions']}")
print(f"- Task completions: {metrics['metrics']['task_completions']}")
print(f"- Errors: {metrics['metrics']['errors']}")
print(f"- Session ID: {metrics['session_id']}")
else:
print("\nTelemetry is disabled")
print("\nTo disable telemetry, set any of these environment variables:")
print("- PRAISONAI_TELEMETRY_DISABLED=true")
print("- DO_NOT_TRACK=true")
+2 -2
View File
@@ -15,7 +15,7 @@ blog_agent = Agent(
role="Blog Writer",
goal="Write a blog post about AI",
backstory="Expert at writing blog posts",
llm="gpt-4o-mini",
llm=llm_config,
)
blog_task = Task(
@@ -27,7 +27,7 @@ blog_task = Task(
agents = PraisonAIAgents(
agents=[blog_agent],
tasks=[blog_task],
memory=True
memory=False
)
result = agents.start()
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""
Final test of automatic telemetry.
"""
print("Step 1: Import praisonaiagents...")
from praisonaiagents import Agent, Task, PraisonAIAgents, get_telemetry
print("\nStep 2: Check if telemetry is initialized...")
telemetry = get_telemetry()
print(f" Telemetry enabled: {telemetry.enabled}")
print(f" PostHog available: {telemetry._posthog is not None}")
print("\nStep 3: Check if Agent class is instrumented...")
print(f" Agent.__init__.__name__: {Agent.__init__.__name__}")
print("\nStep 4: Create an agent...")
agent = Agent(name="Test", role="Test", goal="Test", instructions="Test")
print(f" agent.chat wrapped: {hasattr(agent.chat, '__wrapped__')}")
print("\nStep 5: Create and run a simple workflow...")
task = Task(description="Say hello", expected_output="A greeting", agent=agent)
workflow = PraisonAIAgents(agents=[agent], tasks=[task], process="sequential")
# Check workflow instrumentation
print(f" workflow.execute_task wrapped: {hasattr(workflow.execute_task, '__wrapped__')}")
result = workflow.start()
print(f"\nResult: {result}")
print("\nStep 6: Check metrics...")
metrics = telemetry.get_metrics()
print(f" Metrics: {metrics['metrics']}")
if metrics['metrics']['agent_executions'] > 0:
print("\n✅ Automatic telemetry is working!")
else:
print("\n❌ Automatic telemetry is NOT working")
print("\nPossible reason: The auto_instrument_all() in __init__.py might be")
print("running before the telemetry module's lazy initialization completes.")
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""
Test import order to debug telemetry availability.
"""
import sys
print("1. Importing telemetry module directly...")
try:
import praisonaiagents.telemetry
print(" ✅ Telemetry module imported successfully")
except Exception as e:
print(f" ❌ Failed: {type(e).__name__}: {e}")
print("\n2. Importing main praisonaiagents...")
try:
import praisonaiagents
print(" ✅ Main module imported successfully")
print(f" _telemetry_available: {praisonaiagents._telemetry_available}")
except Exception as e:
print(f" ❌ Failed: {type(e).__name__}: {e}")
print("\n3. Checking what prevented telemetry import...")
# The main __init__.py tries to import from .telemetry
# Let's see if we can import the functions directly
try:
from praisonaiagents.telemetry import (
get_telemetry,
enable_telemetry,
disable_telemetry,
MinimalTelemetry,
TelemetryCollector
)
print(" ✅ All telemetry functions imported successfully")
except Exception as e:
print(f" ❌ Failed to import telemetry functions: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Test script to identify litellm telemetry behavior"""
import os
import sys
# Disable litellm telemetry before importing
os.environ["LITELLM_TELEMETRY"] = "False"
os.environ["LITELLM_LOG"] = "ERROR"
print("Environment variables set:")
print(f"LITELLM_TELEMETRY: {os.environ.get('LITELLM_TELEMETRY')}")
print(f"LITELLM_LOG: {os.environ.get('LITELLM_LOG')}")
print("\nImporting litellm...")
import litellm
print(f"\nChecking litellm telemetry status:")
print(f"Has telemetry attribute: {hasattr(litellm, 'telemetry')}")
if hasattr(litellm, 'telemetry'):
print(f"Telemetry value: {litellm.telemetry}")
# Try to disable telemetry programmatically
if hasattr(litellm, 'telemetry'):
litellm.telemetry = False
print(f"\nTelemetry disabled programmatically")
# Check callbacks
print(f"\nCallbacks: {litellm.callbacks}")
print(f"Success callbacks: {litellm.success_callback}")
print(f"Async success callbacks: {litellm._async_success_callback}")
# Test a simple completion
print("\n\nTesting completion (this might trigger telemetry)...")
try:
response = litellm.completion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Say hi"}],
mock_response="Hi there!"
)
print("Completion successful")
except Exception as e:
print(f"Error: {e}")
print("\nDone. Check if any network requests were made to BerriAI/litellm")
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""
Test manual instrumentation to verify it works.
"""
from praisonaiagents import Agent, Task, PraisonAIAgents
from praisonaiagents.telemetry import get_telemetry
from praisonaiagents.telemetry.integration import auto_instrument_all
# Manually call auto_instrument_all AFTER importing classes
print("1. Calling auto_instrument_all()...")
auto_instrument_all()
print("\n2. Creating agent and task...")
agent = Agent(
name="Calculator",
role="Math Expert",
goal="Perform calculations",
instructions="You are a helpful math expert."
)
task = Task(
description="Calculate 3 + 3",
expected_output="The sum",
agent=agent
)
print("\n3. Creating workflow...")
workflow = PraisonAIAgents(
agents=[agent],
tasks=[task],
process="sequential"
)
print("\n4. Running workflow...")
result = workflow.start()
print(f"Result: {result}")
print("\n5. Checking telemetry metrics...")
telemetry = get_telemetry()
metrics = telemetry.get_metrics()
print(f"Telemetry metrics: {metrics['metrics']}")
print(f"PostHog available: {telemetry._posthog is not None}")
if metrics['metrics']['agent_executions'] > 0:
print("\n✅ SUCCESS! Telemetry is working correctly.")
print(" Data will be sent to PostHog on program exit.")
else:
print("\n❌ FAILED! Telemetry metrics are still 0.")
# Additional debugging
print("\nDebugging info:")
print(f" agent.chat wrapped: {hasattr(agent.chat, '__wrapped__')}")
print(f" workflow.execute_task wrapped: {hasattr(workflow.execute_task, '__wrapped__')}")
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""
Test PostHog integration in telemetry.
"""
import os
import sys
# Ensure telemetry is enabled
if 'PRAISONAI_TELEMETRY_DISABLED' in os.environ:
del os.environ['PRAISONAI_TELEMETRY_DISABLED']
if 'PRAISONAI_DISABLE_TELEMETRY' in os.environ:
del os.environ['PRAISONAI_DISABLE_TELEMETRY']
if 'DO_NOT_TRACK' in os.environ:
del os.environ['DO_NOT_TRACK']
# Test PostHog availability
print("=== Testing PostHog Integration ===")
print("\n1. Checking PostHog import:")
try:
from posthog import Posthog
print("✓ PostHog is available")
except ImportError as e:
print("✗ PostHog is NOT available")
print(f" Error: {e}")
print("\n To fix: pip install posthog")
sys.exit(1)
# Test telemetry initialization
print("\n2. Testing telemetry initialization:")
from praisonaiagents.telemetry import get_telemetry
telemetry = get_telemetry()
print(f"✓ Telemetry enabled: {telemetry.enabled}")
print(f"✓ Session ID: {telemetry.session_id}")
# Check PostHog client
print("\n3. Checking PostHog client:")
if hasattr(telemetry, '_posthog') and telemetry._posthog:
print("✓ PostHog client is initialized")
print(f" API Key: {telemetry._posthog.api_key[:10]}...")
print(f" Host: {telemetry._posthog.host}")
else:
print("✗ PostHog client is NOT initialized")
# Test tracking
print("\n4. Testing event tracking:")
telemetry.track_agent_execution("TestAgent", success=True)
telemetry.track_task_completion("TestTask", success=True)
telemetry.track_tool_usage("test_tool", success=True)
print("✓ Events tracked locally")
# Test flush (which should send to PostHog)
print("\n5. Testing flush (should send to PostHog):")
telemetry.flush()
print("✓ Flush completed")
# Check metrics
print("\n6. Current metrics:")
metrics = telemetry.get_metrics()
for key, value in metrics.items():
print(f" {key}: {value}")
print("\n=== PostHog Test Complete ===")
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""
Detailed test of PostHog integration showing why events might not be sent.
"""
import os
import sys
import time
# Ensure telemetry is enabled
for var in ['PRAISONAI_TELEMETRY_DISABLED', 'PRAISONAI_DISABLE_TELEMETRY', 'DO_NOT_TRACK']:
if var in os.environ:
del os.environ[var]
print("=== Detailed PostHog Test ===")
# Test PostHog directly
print("\n1. Testing PostHog directly:")
try:
from posthog import Posthog
# Create a PostHog client directly
posthog_client = Posthog(
project_api_key='phc_skZpl3eFLQJ4iYjsERNMbCO6jfeSJi2vyZlPahKgxZ7',
host='https://eu.i.posthog.com'
)
# Send a test event
print(" Sending test event...")
posthog_client.capture(
distinct_id='test-user-123',
event='direct_test_event',
properties={
'test': True,
'timestamp': time.time()
}
)
# Flush to ensure event is sent
print(" Flushing...")
posthog_client.flush()
print("✓ Direct PostHog test completed")
except Exception as e:
print(f"✗ Direct PostHog test failed: {e}")
import traceback
traceback.print_exc()
# Test telemetry integration
print("\n2. Testing telemetry integration:")
from praisonaiagents.telemetry.telemetry import MinimalTelemetry
# Create a new telemetry instance with debug output
class DebugTelemetry(MinimalTelemetry):
def flush(self):
"""Override flush to add debug output."""
if not self.enabled:
print(" [DEBUG] Telemetry disabled, skipping flush")
return
metrics = self.get_metrics()
print(f" [DEBUG] Flushing metrics: {metrics}")
# Check PostHog client
if hasattr(self, '_posthog') and self._posthog:
print(" [DEBUG] PostHog client exists")
print(f" [DEBUG] PostHog API key: {self._posthog.api_key[:20]}...")
print(f" [DEBUG] PostHog host: {self._posthog.host}")
try:
# Send events
print(" [DEBUG] Sending sdk_used event...")
self._posthog.capture(
distinct_id='anonymous',
event='sdk_used',
properties={
'version': self._environment['framework_version'],
'os': self._environment['os_type'],
'python_version': self._environment['python_version'],
'session_id': self.session_id,
'metrics': self._metrics,
'$process_person_profile': False
}
)
print(" [DEBUG] Sending test event...")
self._posthog.capture('test-id', 'test-event')
# Explicitly flush PostHog
print(" [DEBUG] Flushing PostHog client...")
self._posthog.flush()
print(" [DEBUG] PostHog flush completed")
except Exception as e:
print(f" [DEBUG] PostHog error: {e}")
import traceback
traceback.print_exc()
else:
print(" [DEBUG] No PostHog client available")
# Reset counters
for key in self._metrics:
if isinstance(self._metrics[key], int):
self._metrics[key] = 0
# Create debug telemetry instance
telemetry = DebugTelemetry(enabled=True)
# Track some events
print("\n3. Tracking events:")
telemetry.track_agent_execution("TestAgent", success=True)
telemetry.track_task_completion("TestTask", success=True)
telemetry.track_tool_usage("calculator", success=True)
print("✓ Events tracked")
# Flush with debug output
print("\n4. Flushing telemetry:")
telemetry.flush()
# Wait a moment for async operations
print("\n5. Waiting for async operations...")
time.sleep(2)
print("\n=== Test Complete ===")
# Additional debug info
print("\n6. Debug information:")
print(f" POSTHOG_AVAILABLE: {telemetry.logger.parent.manager.loggerDict.get('posthog', 'Not found')}")
print(f" Telemetry module location: {MinimalTelemetry.__module__}")
# Check if PostHog is properly installed
print("\n7. PostHog installation check:")
try:
import posthog
print(f" PostHog version: {posthog.VERSION}")
print(f" PostHog location: {posthog.__file__}")
except Exception as e:
print(f" PostHog check failed: {e}")
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""
Test PostHog integration directly to verify it's working.
"""
import os
from praisonaiagents.telemetry import get_telemetry
from praisonaiagents.telemetry.integration import auto_instrument_all
# Enable automatic telemetry instrumentation
auto_instrument_all()
telemetry = get_telemetry()
print(f"Telemetry enabled: {telemetry.enabled}")
print(f"PostHog available: {telemetry._posthog is not None}")
if telemetry.enabled and telemetry._posthog:
# Manually track some events
telemetry.track_agent_execution("TestAgent", success=True)
telemetry.track_task_completion("TestTask", success=True)
telemetry.track_tool_usage("TestTool")
# Get metrics before flush
metrics = telemetry.get_metrics()
print(f"\nMetrics before flush:")
print(f"- Agent executions: {metrics['metrics']['agent_executions']}")
print(f"- Task completions: {metrics['metrics']['task_completions']}")
print(f"- Tool calls: {metrics['metrics']['tool_calls']}")
# Manually flush to send to PostHog
print("\nFlushing telemetry to PostHog...")
telemetry.flush()
# Get metrics after flush (should be reset)
metrics = telemetry.get_metrics()
print(f"\nMetrics after flush (should be reset):")
print(f"- Agent executions: {metrics['metrics']['agent_executions']}")
print(f"- Task completions: {metrics['metrics']['task_completions']}")
print(f"- Tool calls: {metrics['metrics']['tool_calls']}")
print("\n✅ If no errors above, PostHog integration is working!")
else:
print("\n❌ Telemetry or PostHog is not available")
@@ -0,0 +1,20 @@
#!/usr/bin/env python3
"""
Test PostHog initialization error.
"""
from posthog import Posthog
print("Testing PostHog initialization with telemetry parameters...")
try:
posthog_client = Posthog(
project_api_key='phc_skZpl3eFLQJ4iYjsERNMbCO6jfeSJi2vyZlPahKgxZ7',
host='https://eu.i.posthog.com',
disable_geoip=True,
events_to_ignore=['test-event']
)
print(f"✅ PostHog client created: {posthog_client}")
except Exception as e:
print(f"❌ PostHog initialization failed: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""
Test PostHog import and initialization.
"""
print("1. Testing PostHog import...")
try:
from posthog import Posthog
print("✅ PostHog imported successfully")
except ImportError as e:
print(f"❌ PostHog import failed: {e}")
exit(1)
print("\n2. Testing PostHog initialization...")
try:
posthog_client = Posthog(
project_api_key='phc_skZpl3eFLQJ4iYjsERNMbCO6jfeSJi2vyZlPahKgxZ7',
host='https://eu.i.posthog.com'
)
print("✅ PostHog client created successfully")
except Exception as e:
print(f"❌ PostHog initialization failed: {e}")
exit(1)
print("\n3. Testing PostHog capture...")
try:
posthog_client.capture(
distinct_id='test-user',
event='test-event',
properties={'test': True}
)
print("✅ PostHog capture successful")
except Exception as e:
print(f"❌ PostHog capture failed: {e}")
print("\n4. Testing telemetry module...")
from praisonaiagents.telemetry.telemetry import MinimalTelemetry, POSTHOG_AVAILABLE
print(f"POSTHOG_AVAILABLE in telemetry module: {POSTHOG_AVAILABLE}")
print("\n5. Creating MinimalTelemetry instance...")
telemetry = MinimalTelemetry(enabled=True)
print(f"Telemetry enabled: {telemetry.enabled}")
print(f"Telemetry _posthog: {telemetry._posthog}")
print("\n6. Checking get_telemetry()...")
from praisonaiagents.telemetry import get_telemetry
global_telemetry = get_telemetry()
print(f"Global telemetry enabled: {global_telemetry.enabled}")
print(f"Global telemetry _posthog: {global_telemetry._posthog}")
print(f"Global telemetry session_id: {global_telemetry.session_id}")
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""
Test that telemetry works automatically without manual instrumentation.
"""
# NO manual telemetry setup - it should work automatically!
from praisonaiagents import Agent, Task, PraisonAIAgents
# Create a simple agent
agent = Agent(
name="AutoTelemetryTest",
role="Math Expert",
goal="Perform calculations",
instructions="You are a helpful math expert."
)
# Create a task
task = Task(
description="Calculate 5 + 5",
expected_output="The sum",
agent=agent
)
# Create and run workflow
workflow = PraisonAIAgents(
agents=[agent],
tasks=[task],
process="sequential"
)
print("Running workflow (telemetry should be automatic)...")
result = workflow.start()
print(f"Result: {result}")
# Check if telemetry was collected
from praisonaiagents.telemetry import get_telemetry
telemetry = get_telemetry()
if telemetry.enabled:
metrics = telemetry.get_metrics()
print(f"\n✅ Telemetry is working automatically!")
print(f"- Agent executions: {metrics['metrics']['agent_executions']}")
print(f"- Task completions: {metrics['metrics']['task_completions']}")
print(f"- Session ID: {metrics['session_id']}")
print("\nTelemetry data will be sent to PostHog on program exit.")
else:
print("\n❌ Telemetry is disabled")
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Test to verify litellm telemetry is properly disabled"""
import sys
import os
print("Testing litellm telemetry is disabled...")
# Test 1: Check environment variable is set
print(f"\n1. Environment variable LITELLM_TELEMETRY: {os.environ.get('LITELLM_TELEMETRY', 'NOT SET')}")
# Test 2: Import praisonaiagents and check if telemetry is disabled
try:
from praisonaiagents.llm import LLM
print("2. Successfully imported LLM from praisonaiagents")
# Check if litellm was imported and telemetry is disabled
import litellm
print(f"3. litellm.telemetry = {litellm.telemetry}")
# Test 3: Create an LLM instance
llm = LLM(model="gpt-3.5-turbo")
print("4. Successfully created LLM instance")
# Check telemetry again after instance creation
print(f"5. After LLM creation, litellm.telemetry = {litellm.telemetry}")
# Test 4: Try a mock completion
print("\n6. Testing mock completion...")
response = litellm.completion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "test"}],
mock_response="test response"
)
print(" Mock completion successful")
# Final check
print(f"\n7. Final check: litellm.telemetry = {litellm.telemetry}")
if litellm.telemetry == False:
print("\n✅ SUCCESS: Telemetry is properly disabled!")
else:
print("\n❌ FAILURE: Telemetry is still enabled!")
sys.exit(1)
except Exception as e:
print(f"\n❌ ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
print("\nAll tests passed! Telemetry should be disabled.")
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""
Test to demonstrate why telemetry isn't being sent by default.
"""
import os
# Ensure telemetry is enabled
for var in ['PRAISONAI_TELEMETRY_DISABLED', 'PRAISONAI_DISABLE_TELEMETRY', 'DO_NOT_TRACK']:
if var in os.environ:
del os.environ[var]
print("=== Testing Telemetry Integration ===")
# Test 1: Default behavior (no telemetry)
print("\n1. Testing default Agent behavior:")
from praisonaiagents import Agent, Task
agent = Agent(
name="TestAgent",
role="Tester",
goal="Test telemetry",
backstory="I test things",
llm="gpt-4o-mini"
)
task = Task(
description="Say hello",
expected_output="A greeting",
agent=agent
)
# Check if agent has telemetry
print(f" Agent has telemetry: {hasattr(agent, '_telemetry')}")
print(f" Agent methods: {[m for m in dir(agent) if 'telemetry' in m.lower()]}")
# Test 2: Manual telemetry integration
print("\n2. Testing manual telemetry integration:")
from praisonaiagents.telemetry import get_telemetry
from praisonaiagents.telemetry.integration import instrument_agent
telemetry = get_telemetry()
print(f" Telemetry enabled: {telemetry.enabled}")
# Manually instrument the agent
instrument_agent(agent, telemetry)
print(" Agent instrumented manually")
# Test 3: Auto-instrumentation
print("\n3. Testing auto-instrumentation:")
from praisonaiagents.telemetry.integration import auto_instrument_all
# Enable auto-instrumentation
auto_instrument_all(telemetry)
print(" Auto-instrumentation enabled")
# Create a new agent to test auto-instrumentation
new_agent = Agent(
name="AutoInstrumentedAgent",
role="Tester",
goal="Test auto telemetry",
backstory="I test auto-instrumentation",
llm="gpt-4o-mini"
)
print(f" New agent instrumented: {hasattr(new_agent.execute, '__wrapped__')}")
# Test 4: Check if telemetry data is collected
print("\n4. Checking telemetry metrics:")
initial_metrics = telemetry.get_metrics()
print(f" Initial metrics: {initial_metrics['metrics']}")
# The problem: telemetry is never flushed automatically!
print("\n5. The Issue:")
print(" ✗ Telemetry is implemented but NOT automatically integrated")
print(" ✗ Even if integrated, flush() is never called automatically")
print(" ✗ PostHog events are only sent when flush() is explicitly called")
# Test 5: Solution - manual flush
print("\n6. Solution - Manual flush:")
telemetry.flush()
print(" ✓ Manual flush called - events sent to PostHog")
# Test 6: Workflow integration
print("\n7. Testing workflow integration:")
from praisonaiagents import PraisonAIAgents
agents = [
Agent(name="Agent1", role="First", goal="Do first task", backstory="I'm first"),
Agent(name="Agent2", role="Second", goal="Do second task", backstory="I'm second")
]
tasks = [
Task(description="Task 1", expected_output="Output 1", agent=agents[0]),
Task(description="Task 2", expected_output="Output 2", agent=agents[1])
]
workflow = PraisonAIAgents(
agents=agents,
tasks=tasks,
process="sequential"
)
print(f" Workflow has telemetry integration: {hasattr(workflow.start, '__wrapped__')}")
print("\n=== Summary of Issues ===")
print("\n1. No automatic integration:")
print(" - Agent and PraisonAIAgents classes don't automatically use telemetry")
print(" - Need to manually call instrument_agent() or auto_instrument_all()")
print("\n2. No automatic flush:")
print(" - Even when integrated, telemetry data is only collected locally")
print(" - flush() must be called explicitly to send data to PostHog")
print(" - No automatic flush on program exit or periodic flush")
print("\n3. Missing integration points:")
print(" - Agent.__init__ doesn't initialize telemetry")
print(" - Agent.execute() doesn't track execution")
print(" - PraisonAIAgents doesn't track workflow execution")
print(" - No atexit handler to flush on program termination")
print("\n=== Recommendations ===")
print("\n1. Add automatic integration in Agent.__init__ and PraisonAIAgents.__init__")
print("2. Add atexit handler to flush telemetry on program exit")
print("3. Add periodic flush (e.g., every 100 events or 60 seconds)")
print("4. Document telemetry behavior clearly for users")
@@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""
Simple test to check telemetry.
"""
from praisonaiagents import Agent, Task, PraisonAIAgents
from praisonaiagents.telemetry import get_telemetry
# Create workflow
agent = Agent(name="Test", role="Tester", goal="Test", instructions="Test")
task = Task(description="Test", expected_output="Test", agent=agent)
workflow = PraisonAIAgents(agents=[agent], tasks=[task], process="sequential")
# Run workflow
print("Running workflow...")
result = workflow.start()
# Check telemetry
telemetry = get_telemetry()
metrics = telemetry.get_metrics()
print(f"\nTelemetry metrics: {metrics['metrics']}")
print(f"Session ID: {metrics['session_id']}")
# Check if telemetry will be sent
print(f"\nTelemetry enabled: {telemetry.enabled}")
print(f"PostHog client available: {telemetry._posthog is not None}")
if telemetry._posthog:
print("\n✅ Telemetry will be sent to PostHog on program exit")
@@ -0,0 +1,294 @@
"""
State-Based Workflow Control Example
===================================
This example shows how to use state for:
1. Conditional task execution based on state
2. Loop control using state
3. Dynamic workflow modification
4. State-based error handling and recovery
"""
from praisonaiagents import Agent, Task, PraisonAIAgents
from typing import Dict, Any, List
import random
import json
# Tool functions that use state for control flow
def analyze_data_quality() -> Dict[str, Any]:
"""Analyze data and set quality state"""
# Simulate data quality check
quality_score = random.uniform(0.5, 1.0)
has_errors = random.choice([True, False])
# Set state based on analysis
workflow.set_state("quality_score", quality_score)
workflow.set_state("has_errors", has_errors)
workflow.set_state("data_status", "analyzed")
if quality_score < 0.7:
workflow.set_state("quality_level", "poor")
elif quality_score < 0.85:
workflow.set_state("quality_level", "moderate")
else:
workflow.set_state("quality_level", "good")
return {
"quality_score": quality_score,
"has_errors": has_errors,
"quality_level": workflow.get_state("quality_level"),
"recommendation": "clean_data" if has_errors or quality_score < 0.8 else "proceed"
}
def clean_data_based_on_state() -> Dict[str, Any]:
"""Clean data based on current state"""
quality_score = workflow.get_state("quality_score", 0)
has_errors = workflow.get_state("has_errors", False)
cleaning_attempts = workflow.get_state("cleaning_attempts", 0)
# Increment cleaning attempts
workflow.set_state("cleaning_attempts", cleaning_attempts + 1)
# Simulate cleaning improvement
improvement = random.uniform(0.1, 0.2)
new_quality = min(quality_score + improvement, 0.95)
workflow.set_state("quality_score", new_quality)
workflow.set_state("has_errors", False)
workflow.set_state("last_cleaning_improvement", improvement)
# Determine if more cleaning needed
if new_quality < 0.8 and cleaning_attempts < 3:
status = "needs_more_cleaning"
else:
status = "cleaning_complete"
workflow.set_state("data_status", "cleaned")
return {
"previous_score": quality_score,
"new_score": new_quality,
"improvement": improvement,
"attempts": cleaning_attempts + 1,
"status": status
}
def process_batch_with_state() -> Dict[str, Any]:
"""Process data in batches using state to track progress"""
# Initialize batch processing state
if not workflow.has_state("batch_total"):
batch_total = random.randint(5, 10)
workflow.set_state("batch_total", batch_total)
workflow.set_state("batch_current", 0)
workflow.set_state("batch_results", [])
# Get current state
batch_total = workflow.get_state("batch_total")
batch_current = workflow.get_state("batch_current")
batch_results = workflow.get_state("batch_results", [])
# Process current batch
batch_current += 1
result = {
"batch_number": batch_current,
"records_processed": random.randint(100, 500),
"errors": random.randint(0, 5)
}
batch_results.append(result)
# Update state
workflow.set_state("batch_current", batch_current)
workflow.set_state("batch_results", batch_results)
# Determine if more batches
if batch_current < batch_total:
status = "more_batches"
workflow.set_state("batch_status", "in_progress")
else:
status = "all_batches_complete"
workflow.set_state("batch_status", "completed")
workflow.set_state("total_records", sum(r["records_processed"] for r in batch_results))
workflow.set_state("total_errors", sum(r["errors"] for r in batch_results))
return {
"batch": result,
"progress": f"{batch_current}/{batch_total}",
"status": status,
"remaining": batch_total - batch_current
}
def generate_report_from_state() -> Dict[str, Any]:
"""Generate comprehensive report from accumulated state"""
# Gather all relevant state
report = {
"data_quality": {
"initial_score": workflow.get_state("quality_score", 0),
"quality_level": workflow.get_state("quality_level", "unknown"),
"cleaning_attempts": workflow.get_state("cleaning_attempts", 0),
"final_status": workflow.get_state("data_status", "unknown")
},
"batch_processing": {
"total_batches": workflow.get_state("batch_total", 0),
"completed_batches": workflow.get_state("batch_current", 0),
"total_records": workflow.get_state("total_records", 0),
"total_errors": workflow.get_state("total_errors", 0),
"status": workflow.get_state("batch_status", "not_started")
},
"workflow_metadata": {
"has_errors": workflow.get_state("has_errors", False),
"state_keys": list(workflow.get_all_state().keys()),
"state_size": len(workflow.get_all_state())
}
}
# Calculate summary metrics
if workflow.has_state("batch_results"):
batch_results = workflow.get_state("batch_results")
report["batch_processing"]["average_records_per_batch"] = (
sum(r["records_processed"] for r in batch_results) / len(batch_results)
if batch_results else 0
)
# Set final state
workflow.set_state("final_report_generated", True)
workflow.set_state("workflow_complete", True)
return report
def check_state_conditions() -> str:
"""Check various state conditions for decision making"""
quality_level = workflow.get_state("quality_level", "unknown")
cleaning_attempts = workflow.get_state("cleaning_attempts", 0)
has_errors = workflow.get_state("has_errors", False)
if quality_level == "poor" and cleaning_attempts == 0:
return "needs_cleaning"
elif quality_level == "moderate" and not has_errors:
return "can_proceed"
elif quality_level == "good":
return "ready_for_processing"
elif cleaning_attempts >= 3:
return "max_cleaning_reached"
else:
return "needs_analysis"
# Create agents
data_analyst = Agent(
name="DataAnalyst",
role="Data quality analysis",
goal="Analyze data quality and set appropriate state",
backstory="Expert in data quality assessment",
tools=[analyze_data_quality, check_state_conditions],
llm="gpt-4o-mini"
)
data_engineer = Agent(
name="DataEngineer",
role="Data cleaning and processing",
goal="Clean data based on state and process in batches",
backstory="Specialist in data transformation and batch processing",
tools=[clean_data_based_on_state, process_batch_with_state],
llm="gpt-4o-mini"
)
report_generator = Agent(
name="ReportGenerator",
role="Report generation",
goal="Generate comprehensive reports from workflow state",
backstory="Expert in creating detailed analytical reports",
tools=[generate_report_from_state],
llm="gpt-4o-mini"
)
# Create tasks with state-based conditions
analyze_task = Task(
name="analyze_quality",
description="Analyze the data quality and set initial state values",
expected_output="Data quality analysis with recommendations",
agent=data_analyst,
tools=[analyze_data_quality]
)
decision_task = Task(
name="quality_decision",
description="Check data quality state and decide next action using check_state_conditions tool",
expected_output="Decision on whether to clean data or proceed",
agent=data_analyst,
tools=[check_state_conditions],
task_type="decision",
condition={
"needs_cleaning": ["clean_data"],
"can_proceed": ["process_batches"],
"ready_for_processing": ["process_batches"],
"max_cleaning_reached": ["process_batches"],
"needs_analysis": ["analyze_quality"]
},
context=[analyze_task]
)
clean_task = Task(
name="clean_data",
description="Clean the data to improve quality score. Check if more cleaning is needed.",
expected_output="Cleaning results with status",
agent=data_engineer,
tools=[clean_data_based_on_state],
task_type="decision",
condition={
"needs_more_cleaning": ["clean_data"],
"cleaning_complete": ["process_batches"]
}
)
process_task = Task(
name="process_batches",
description="Process data in batches. Continue until all batches are complete.",
expected_output="Batch processing results",
agent=data_engineer,
tools=[process_batch_with_state],
task_type="loop",
condition={
"more_batches": ["process_batches"],
"all_batches_complete": ["generate_report"]
}
)
report_task = Task(
name="generate_report",
description="Generate final report from all accumulated state data",
expected_output="Comprehensive workflow report",
agent=report_generator,
tools=[generate_report_from_state]
)
# Create workflow
workflow = PraisonAIAgents(
agents=[data_analyst, data_engineer, report_generator],
tasks=[analyze_task, decision_task, clean_task, process_task, report_task],
verbose=1,
process="workflow"
)
# Demonstrate state before workflow
print("\n=== State-Based Workflow Control Demo ===")
print("\n1. Initial state (empty):", workflow.get_all_state())
# Run the workflow
print("\n2. Running workflow with state-based decisions...")
result = workflow.start()
# Show final state
print("\n3. Final workflow state:")
final_state = workflow.get_all_state()
print(f" Total state entries: {len(final_state)}")
print(f" Quality level: {final_state.get('quality_level')}")
print(f" Cleaning attempts: {final_state.get('cleaning_attempts', 0)}")
print(f" Batches processed: {final_state.get('batch_current', 0)}/{final_state.get('batch_total', 0)}")
print(f" Total records: {final_state.get('total_records', 0)}")
print(f" Workflow complete: {final_state.get('workflow_complete', False)}")
# Display task execution path
print("\n4. Task Execution Path:")
for task_name, task in workflow.tasks.items():
if hasattr(task, 'status'):
print(f" {task_name}: {task.status}")
print("\n=== Demo Complete ===")
@@ -0,0 +1,259 @@
"""
State Management Example for PraisonAI Agents
============================================
This example demonstrates various state management features:
1. Workflow-level state management
2. Session-level state persistence
3. State sharing between agents
4. State-based decision making
"""
from praisonaiagents import Agent, Task, PraisonAIAgents, Session
from typing import Dict, Any
import json
import time
# Create a session for persistent state
session = Session(session_id="state_demo_session", user_id="demo_user")
# Tool functions that interact with state
def initialize_project_state() -> Dict[str, Any]:
"""Initialize project state with default values"""
# Access workflow state from global workflow variable
workflow.set_state("project_name", "AI Assistant Project")
workflow.set_state("stage", "planning")
workflow.set_state("features", [])
workflow.set_state("completed_features", [])
workflow.set_state("budget", 100000)
workflow.set_state("spent", 0)
# Also save to session for persistence
session.set_state("last_project", "AI Assistant Project")
session.set_state("total_projects", session.get_state("total_projects", 0) + 1)
return {
"status": "initialized",
"project_name": workflow.get_state("project_name"),
"budget": workflow.get_state("budget"),
"session_projects": session.get_state("total_projects")
}
def add_feature_to_project(feature_name: str, estimated_cost: int) -> Dict[str, Any]:
"""Add a feature to the project and update state"""
features = workflow.get_state("features", [])
budget = workflow.get_state("budget", 0)
spent = workflow.get_state("spent", 0)
if spent + estimated_cost > budget:
return {
"status": "rejected",
"reason": "over_budget",
"available_budget": budget - spent
}
feature = {
"name": feature_name,
"cost": estimated_cost,
"status": "planned"
}
features.append(feature)
workflow.set_state("features", features)
workflow.set_state("spent", spent + estimated_cost)
return {
"status": "added",
"feature": feature,
"total_features": len(features),
"remaining_budget": budget - (spent + estimated_cost)
}
def implement_next_feature() -> Dict[str, Any]:
"""Implement the next planned feature"""
features = workflow.get_state("features", [])
completed = workflow.get_state("completed_features", [])
# Find next planned feature
next_feature = None
for i, feature in enumerate(features):
if feature["status"] == "planned":
next_feature = feature
next_feature["status"] = "completed"
features[i] = next_feature
break
if not next_feature:
workflow.set_state("stage", "completed")
return {
"status": "no_more_features",
"completed_count": len(completed),
"stage": "completed"
}
completed.append(next_feature["name"])
workflow.set_state("features", features)
workflow.set_state("completed_features", completed)
workflow.set_state("stage", "implementing")
# Update session state
session.set_state("last_implemented_feature", next_feature["name"])
return {
"status": "implemented",
"feature": next_feature,
"completed_count": len(completed),
"remaining_count": sum(1 for f in features if f["status"] == "planned")
}
def check_project_status() -> Dict[str, Any]:
"""Check the current project status from state"""
return {
"project_name": workflow.get_state("project_name"),
"stage": workflow.get_state("stage"),
"total_features": len(workflow.get_state("features", [])),
"completed_features": len(workflow.get_state("completed_features", [])),
"budget": workflow.get_state("budget"),
"spent": workflow.get_state("spent"),
"remaining": workflow.get_state("budget", 0) - workflow.get_state("spent", 0),
"has_more_features": workflow.has_state("features") and any(
f["status"] == "planned" for f in workflow.get_state("features", [])
),
"all_state": workflow.get_all_state()
}
def retrieve_session_history() -> Dict[str, Any]:
"""Retrieve historical data from session state"""
session_state = session.restore_state()
return {
"total_projects": session.get_state("total_projects", 0),
"last_project": session.get_state("last_project"),
"last_implemented_feature": session.get_state("last_implemented_feature"),
"session_id": session.session_id,
"full_session_state": session_state
}
# Create agents with state-aware tools
project_manager = Agent(
name="ProjectManager",
role="Project planning and management",
goal="Initialize and manage project state effectively",
backstory="Experienced project manager who tracks all project details",
tools=[initialize_project_state, add_feature_to_project, check_project_status],
llm="gpt-4o-mini"
)
developer = Agent(
name="Developer",
role="Feature implementation",
goal="Implement features based on project state",
backstory="Senior developer who implements features systematically",
tools=[implement_next_feature, check_project_status],
llm="gpt-4o-mini"
)
analyst = Agent(
name="Analyst",
role="Project analysis and reporting",
goal="Analyze project state and provide insights",
backstory="Data analyst who provides comprehensive project reports",
tools=[check_project_status, retrieve_session_history],
llm="gpt-4o-mini"
)
# Create tasks that utilize state
init_task = Task(
name="initialize_project",
description="Initialize the project state with default values",
expected_output="Project initialization status with budget and name",
agent=project_manager,
tools=[initialize_project_state]
)
plan_features_task = Task(
name="plan_features",
description="""Add the following features to the project:
1. User Authentication (cost: 15000)
2. Data Analytics Dashboard (cost: 25000)
3. API Integration (cost: 20000)
4. Mobile App Support (cost: 30000)
5. Advanced Reporting (cost: 20000)
Use the add_feature_to_project tool for each feature.""",
expected_output="List of features added with budget status",
agent=project_manager,
tools=[add_feature_to_project],
context=[init_task]
)
implement_features_task = Task(
name="implement_features",
description="""Implement all planned features one by one.
Use implement_next_feature tool repeatedly until all features are completed.
Check project status after each implementation.""",
expected_output="Implementation progress and final status",
agent=developer,
tools=[implement_next_feature, check_project_status],
context=[plan_features_task]
)
analyze_project_task = Task(
name="analyze_project",
description="""Provide a comprehensive analysis of the project including:
1. Current project state and completion status
2. Budget utilization
3. Historical session data
4. Summary of all state information""",
expected_output="Detailed project analysis report",
agent=analyst,
tools=[check_project_status, retrieve_session_history],
context=[implement_features_task]
)
# Create workflow with state management
workflow = PraisonAIAgents(
agents=[project_manager, developer, analyst],
tasks=[init_task, plan_features_task, implement_features_task, analyze_project_task],
verbose=1,
process="sequential"
)
# Demonstrate state operations before starting
print("\n=== State Management Demo ===")
print("\n1. Initial State Check:")
print(f" Has 'project_name' state: {workflow.has_state('project_name')}")
print(f" All state: {workflow.get_all_state()}")
# Run the workflow
print("\n2. Running Workflow...")
result = workflow.start()
# Demonstrate state after workflow
print("\n3. Final State Check:")
print(f" Project Name: {workflow.get_state('project_name')}")
print(f" Stage: {workflow.get_state('stage')}")
print(f" Features: {len(workflow.get_state('features', []))}")
print(f" Completed: {workflow.get_state('completed_features', [])}")
print(f" Budget Spent: ${workflow.get_state('spent')}/{workflow.get_state('budget')}")
# Test state persistence
print("\n4. Session State Persistence:")
print(f" Total Projects (this session): {session.get_state('total_projects')}")
print(f" Last Project: {session.get_state('last_project')}")
# Save session state
session.save_state({"workflow_completed": True, "timestamp": time.time()})
# Demonstrate state update and deletion
print("\n5. State Manipulation:")
workflow.update_state({"additional_notes": "Project completed successfully"})
print(f" After update: {workflow.get_state('additional_notes')}")
workflow.delete_state("additional_notes")
print(f" After delete, has 'additional_notes': {workflow.has_state('additional_notes')}")
# Clear all workflow state (optional)
# workflow.clear_state()
# print(f" After clear: {workflow.get_all_state()}")
print("\n=== State Management Demo Complete ===")
@@ -0,0 +1,397 @@
"""
State with Memory Integration Example
====================================
This example demonstrates:
1. Using shared memory for state persistence
2. State sharing between agents via memory
3. Combining workflow state with memory storage
4. Historical state tracking
"""
from praisonaiagents import Agent, Task, PraisonAIAgents
from praisonaiagents.memory import Memory
from typing import Dict, Any, List
import json
import time
from datetime import datetime
# Initialize shared memory
memory_config = {
"config": {
"collection_name": "state_memory_demo",
"path": "./state_memory_db"
}
}
shared_memory = Memory(**memory_config)
# Tool functions that use both state and memory
def initialize_conversation_state(user_name: str, topic: str) -> Dict[str, Any]:
"""Initialize conversation state and store in memory"""
# Set workflow state
workflow.set_state("user_name", user_name)
workflow.set_state("topic", topic)
workflow.set_state("start_time", time.time())
workflow.set_state("turn_count", 0)
workflow.set_state("conversation_history", [])
# Store in memory for persistence
context = f"Starting conversation with {user_name} about {topic}"
shared_memory.add(
text=context,
metadata={
"type": "conversation_start",
"user": user_name,
"topic": topic,
"timestamp": datetime.now().isoformat()
}
)
return {
"status": "initialized",
"user": user_name,
"topic": topic,
"memory_stored": True
}
def add_conversation_turn(speaker: str, message: str) -> Dict[str, Any]:
"""Add a conversation turn to state and memory"""
# Get current state
history = workflow.get_state("conversation_history", [])
turn_count = workflow.get_state("turn_count", 0)
# Create turn entry
turn = {
"turn": turn_count + 1,
"speaker": speaker,
"message": message,
"timestamp": time.time()
}
# Update state
history.append(turn)
workflow.set_state("conversation_history", history)
workflow.set_state("turn_count", turn_count + 1)
workflow.set_state(f"last_{speaker}_message", message)
# Store in memory
shared_memory.add(
text=f"{speaker}: {message}",
metadata={
"type": "conversation_turn",
"speaker": speaker,
"turn": turn_count + 1,
"topic": workflow.get_state("topic"),
"user": workflow.get_state("user_name")
}
)
return {
"turn_added": turn,
"total_turns": turn_count + 1,
"history_length": len(history)
}
def search_conversation_memory(query: str, speaker: str = None) -> Dict[str, Any]:
"""Search through conversation memory"""
# Build metadata filter
metadata_filter = {"type": "conversation_turn"}
if speaker:
metadata_filter["speaker"] = speaker
# Search memory
results = shared_memory.search(
query=query,
n=5,
metadata_filter=metadata_filter
)
# Update state with search results
workflow.set_state("last_search_query", query)
workflow.set_state("last_search_results", len(results))
return {
"query": query,
"results_count": len(results),
"results": [
{
"text": r["text"],
"speaker": r["metadata"].get("speaker"),
"turn": r["metadata"].get("turn"),
"relevance": r.get("score", 0)
}
for r in results
]
}
def analyze_conversation_patterns() -> Dict[str, Any]:
"""Analyze patterns in conversation using state and memory"""
# Get state data
history = workflow.get_state("conversation_history", [])
user_name = workflow.get_state("user_name", "Unknown")
topic = workflow.get_state("topic", "Unknown")
# Calculate metrics from state
total_turns = len(history)
speakers = {}
for turn in history:
speaker = turn["speaker"]
speakers[speaker] = speakers.get(speaker, 0) + 1
# Search memory for related conversations
related_convos = shared_memory.search(
query=topic,
n=10,
metadata_filter={"type": "conversation_turn"}
)
# Analyze patterns
patterns = {
"current_conversation": {
"user": user_name,
"topic": topic,
"total_turns": total_turns,
"speaker_distribution": speakers,
"duration_seconds": time.time() - workflow.get_state("start_time", time.time())
},
"memory_insights": {
"related_conversations": len(related_convos),
"common_speakers": list(set(r["metadata"].get("speaker") for r in related_convos)),
"topics_discussed": list(set(r["metadata"].get("topic") for r in related_convos))
}
}
# Store analysis in state
workflow.set_state("conversation_analysis", patterns)
return patterns
def retrieve_user_history(user_name: str) -> Dict[str, Any]:
"""Retrieve all historical data for a user from memory"""
# Search memory for user's conversations
user_history = shared_memory.search(
query=user_name,
n=20,
metadata_filter={"user": user_name}
)
# Organize by conversation topics
topics = {}
for item in user_history:
topic = item["metadata"].get("topic", "Unknown")
if topic not in topics:
topics[topic] = []
topics[topic].append({
"text": item["text"],
"type": item["metadata"].get("type"),
"timestamp": item["metadata"].get("timestamp")
})
# Update state with user history
workflow.set_state("user_history_retrieved", True)
workflow.set_state("user_topics", list(topics.keys()))
return {
"user": user_name,
"total_interactions": len(user_history),
"topics_discussed": list(topics.keys()),
"conversation_count": len(topics),
"history_by_topic": topics
}
def summarize_and_save_state() -> Dict[str, Any]:
"""Create a summary of the conversation and save final state"""
# Get all conversation data
history = workflow.get_state("conversation_history", [])
analysis = workflow.get_state("conversation_analysis", {})
user_name = workflow.get_state("user_name")
topic = workflow.get_state("topic")
# Create summary
summary = {
"conversation_id": f"{user_name}_{topic}_{int(time.time())}",
"user": user_name,
"topic": topic,
"total_turns": len(history),
"duration": time.time() - workflow.get_state("start_time", 0),
"participants": list(set(turn["speaker"] for turn in history)),
"key_points": [turn["message"][:50] + "..." for turn in history[-3:]], # Last 3 messages
"analysis": analysis
}
# Save summary to memory
shared_memory.add(
text=json.dumps(summary, indent=2),
metadata={
"type": "conversation_summary",
"user": user_name,
"topic": topic,
"timestamp": datetime.now().isoformat(),
"conversation_id": summary["conversation_id"]
}
)
# Clear workflow state but keep summary
workflow.set_state("final_summary", summary)
workflow.set_state("state_saved_to_memory", True)
return summary
# Create agents with memory-aware tools
conversation_manager = Agent(
name="ConversationManager",
role="Manage conversation state and memory",
goal="Initialize and track conversation state effectively",
backstory="Expert in conversation management and state tracking",
tools=[initialize_conversation_state, add_conversation_turn, summarize_and_save_state],
memory=shared_memory,
llm="gpt-4o-mini"
)
memory_analyst = Agent(
name="MemoryAnalyst",
role="Analyze conversation patterns from memory",
goal="Extract insights from conversation history",
backstory="Specialist in pattern recognition and memory analysis",
tools=[search_conversation_memory, analyze_conversation_patterns, retrieve_user_history],
memory=shared_memory,
llm="gpt-4o-mini"
)
# Create tasks
init_conversation_task = Task(
name="init_conversation",
description="Initialize a conversation with user 'Alice' about 'AI and Future of Work'",
expected_output="Initialization status",
agent=conversation_manager,
tools=[initialize_conversation_state]
)
simulate_conversation_task = Task(
name="simulate_conversation",
description="""Simulate a conversation by adding these turns:
1. Alice: "What are the main impacts of AI on employment?"
2. Assistant: "AI is transforming employment through automation and augmentation."
3. Alice: "Which industries are most affected?"
4. Assistant: "Manufacturing, customer service, and data analysis see significant changes."
5. Alice: "How can workers prepare for these changes?"
Use add_conversation_turn for each message.""",
expected_output="Conversation simulation results",
agent=conversation_manager,
tools=[add_conversation_turn],
context=[init_conversation_task]
)
search_memory_task = Task(
name="search_memory",
description="""Search conversation memory for:
1. Messages about 'automation'
2. All messages from 'Alice'
3. Messages about 'employment'""",
expected_output="Search results from memory",
agent=memory_analyst,
tools=[search_conversation_memory],
context=[simulate_conversation_task]
)
analyze_patterns_task = Task(
name="analyze_patterns",
description="Analyze conversation patterns and extract insights",
expected_output="Pattern analysis results",
agent=memory_analyst,
tools=[analyze_conversation_patterns],
context=[search_memory_task]
)
retrieve_history_task = Task(
name="retrieve_history",
description="Retrieve all historical data for user 'Alice'",
expected_output="User history summary",
agent=memory_analyst,
tools=[retrieve_user_history],
context=[analyze_patterns_task]
)
save_state_task = Task(
name="save_state",
description="Create final summary and save conversation state to memory",
expected_output="Final conversation summary",
agent=conversation_manager,
tools=[summarize_and_save_state],
context=[retrieve_history_task]
)
# Create workflow with shared memory
workflow = PraisonAIAgents(
agents=[conversation_manager, memory_analyst],
tasks=[
init_conversation_task,
simulate_conversation_task,
search_memory_task,
analyze_patterns_task,
retrieve_history_task,
save_state_task
],
memory=memory_config, # This creates shared memory for all agents
verbose=1,
process="sequential"
)
# Run workflow
print("\n=== State with Memory Integration Demo ===")
print("\n1. Starting workflow with memory-based state management...")
result = workflow.start()
# Display results
print("\n2. Final State Summary:")
final_summary = workflow.get_state("final_summary", {})
if final_summary:
print(f" Conversation ID: {final_summary.get('conversation_id')}")
print(f" Total Turns: {final_summary.get('total_turns')}")
print(f" Duration: {final_summary.get('duration', 0):.2f} seconds")
print(f" Participants: {final_summary.get('participants')}")
print("\n3. Memory Storage Status:")
print(f" State saved to memory: {workflow.get_state('state_saved_to_memory', False)}")
print(f" User history retrieved: {workflow.get_state('user_history_retrieved', False)}")
print(f" Topics in memory: {workflow.get_state('user_topics', [])}")
# Test memory persistence by creating new workflow instance
print("\n4. Testing Memory Persistence:")
print(" Creating new workflow instance to test memory retrieval...")
# Create a simple test agent to verify memory persistence
test_agent = Agent(
name="MemoryTester",
role="Test memory persistence",
goal="Verify stored conversation data",
backstory="Memory system tester",
tools=[search_conversation_memory],
memory=shared_memory,
llm="gpt-4o-mini"
)
test_task = Task(
name="test_memory",
description="Search memory for 'Alice' to verify persistence",
expected_output="Memory search results",
agent=test_agent,
tools=[search_conversation_memory]
)
test_workflow = PraisonAIAgents(
agents=[test_agent],
tasks=[test_task],
memory=memory_config,
verbose=0
)
# This will use the same memory collection and find previously stored data
test_result = test_workflow.start()
print("\n=== Demo Complete ===")
print("\nNote: The conversation data has been persisted in memory and can be")
print("retrieved in future sessions using the same memory configuration.")
@@ -0,0 +1,258 @@
"""
Example demonstrating telemetry usage in PraisonAI Agents.
This example shows how to:
1. Enable telemetry with OpenTelemetry backend
2. Use automatic instrumentation
3. Use manual instrumentation
4. Export telemetry data
"""
import os
from praisonaiagents import (
Agent, Task, PraisonAIAgents,
enable_telemetry, disable_telemetry, get_telemetry_collector
)
from praisonaiagents.tools import DuckDuckGoSearchTool
# Set current path to package root directory
import os
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def basic_telemetry_example():
"""Basic example with automatic telemetry."""
print("\n=== Basic Telemetry Example ===\n")
# Enable telemetry - it will automatically instrument all agents and tasks
telemetry = enable_telemetry(
backend="opentelemetry",
service_name="praisonai-demo",
exporter="console" # Use console exporter for demo
)
if not telemetry:
print("Telemetry dependencies not installed. Install with:")
print("pip install praisonaiagents[telemetry]")
return
# Create agents - telemetry will be automatically added
researcher = Agent(
name="Researcher",
role="Information gatherer",
goal="Find accurate information about topics",
backstory="You are an expert researcher with attention to detail",
tools=[DuckDuckGoSearchTool()],
llm="gpt-4o-mini"
)
writer = Agent(
name="Writer",
role="Content creator",
goal="Create engaging content based on research",
backstory="You are a skilled writer who creates clear, engaging content",
llm="gpt-4o-mini"
)
# Create tasks
research_task = Task(
name="research_task",
description="Research the latest developments in quantum computing",
expected_output="A summary of recent quantum computing breakthroughs",
agent=researcher
)
writing_task = Task(
name="writing_task",
description="Write a blog post about quantum computing developments",
expected_output="A 300-word blog post suitable for a general audience",
agent=writer,
context=[research_task] # Depends on research task
)
# Create workflow
workflow = PraisonAIAgents(
agents=[researcher, writer],
tasks=[research_task, writing_task],
process="sequential",
verbose=True
)
# Run workflow - all telemetry will be collected automatically
print("Running workflow with telemetry enabled...\n")
result = workflow.start()
# Get telemetry metrics
metrics = telemetry.get_metrics()
print("\n=== Telemetry Metrics ===")
print(f"Agent executions: {metrics['agent_executions']}")
print(f"Task completions: {metrics['task_completions']}")
print(f"Tool calls: {metrics['tool_calls']}")
print(f"LLM calls: {metrics['llm_calls']}")
print(f"Total tokens: {metrics['total_tokens']}")
print(f"Errors: {metrics['errors']}")
# Disable telemetry
disable_telemetry()
return result
def manual_telemetry_example():
"""Example with manual telemetry instrumentation."""
print("\n=== Manual Telemetry Example ===\n")
# Enable telemetry
telemetry = enable_telemetry(backend="custom") # Use logging backend
if not telemetry:
return
# Create agent without automatic instrumentation
agent = Agent(
name="Assistant",
role="General assistant",
goal="Help with various tasks",
backstory="You are a helpful AI assistant",
llm="gpt-4o-mini"
)
# Manual telemetry - trace custom operations
with telemetry.trace_agent_execution("Assistant", custom_field="demo"):
response = agent.chat("What is the capital of France?")
print(f"Response: {response}")
# Use telemetry decorator for functions
@telemetry.trace(span_type="custom_operation")
def process_data(data):
"""Process some data with telemetry."""
# Simulate processing
import time
time.sleep(0.1)
# Record custom metrics
telemetry.record_metric("data.processed", len(data), {"type": "text"})
return data.upper()
# Call traced function
result = process_data("hello world")
print(f"Processed: {result}")
# Manual event recording
telemetry._backend.record_event("custom_event", {
"user_action": "manual_example",
"success": True
})
disable_telemetry()
def advanced_telemetry_example():
"""Advanced example with custom telemetry backend and OTLP export."""
print("\n=== Advanced Telemetry Example ===\n")
# For OTLP export, you would typically have an OTLP collector running
# For this example, we'll use console export
# Enable telemetry with OTLP configuration
telemetry = enable_telemetry(
backend="opentelemetry",
service_name="praisonai-production",
service_version="1.0.0",
exporter="otlp", # Would export to OTLP collector
otlp_endpoint="localhost:4317", # OTLP collector endpoint
metric_export_interval=10000 # Export metrics every 10 seconds
)
if not telemetry:
print("Telemetry not available. Using fallback.")
# Even without telemetry, the code still works
agent = Agent(
name="Analyst",
role="Data analyst",
goal="Analyze data and provide insights",
backstory="You are an experienced data analyst",
llm="gpt-4o-mini"
)
result = agent.chat("Analyze the trend: [10, 15, 13, 18, 22, 20, 25]")
print(f"Analysis: {result}")
return
# Create instrumented agent
agent = Agent(
name="Analyst",
role="Data analyst",
goal="Analyze data and provide insights",
backstory="You are an experienced data analyst",
llm="gpt-4o-mini"
)
# Use context manager for grouped operations
with telemetry.trace("analysis_workflow", workflow_type="data_analysis"):
# Multiple operations within the same trace context
response1 = agent.chat("What is the mean of: [10, 15, 13, 18, 22, 20, 25]")
response2 = agent.chat("What is the trend in this data?")
response3 = agent.chat("Predict the next 3 values")
# Record custom metrics
telemetry.record_metric("analysis.steps", 3)
telemetry.record_cost(0.001, model="gpt-4o-mini") # Track costs
print("\nAnalysis complete. Telemetry data exported to OTLP collector.")
disable_telemetry()
def telemetry_with_errors_example():
"""Example showing how telemetry handles errors."""
print("\n=== Telemetry with Error Handling ===\n")
telemetry = enable_telemetry()
if not telemetry:
return
agent = Agent(
name="ErrorProneAgent",
role="Test agent",
goal="Test error handling",
backstory="An agent for testing",
llm="gpt-4o-mini"
)
# This will be traced and errors will be recorded
try:
with telemetry.trace_agent_execution("ErrorProneAgent"):
# Simulate an error by using an invalid tool
agent.execute_tool("non_existent_tool", "test")
except Exception as e:
print(f"Caught error: {e}")
print("Error was recorded in telemetry")
# Check error metrics
metrics = telemetry.get_metrics()
print(f"\nError count in telemetry: {metrics['errors']}")
disable_telemetry()
if __name__ == "__main__":
print("PraisonAI Agents Telemetry Examples")
print("===================================")
# Run basic example
basic_telemetry_example()
# Run manual instrumentation example
manual_telemetry_example()
# Run advanced example (commented out as it requires OTLP collector)
# advanced_telemetry_example()
# Run error handling example
telemetry_with_errors_example()
print("\n✅ All telemetry examples completed!")
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""
Test PostHog integration after fix.
"""
import os
import time
# Ensure telemetry is enabled
for var in ['PRAISONAI_TELEMETRY_DISABLED', 'PRAISONAI_DISABLE_TELEMETRY', 'DO_NOT_TRACK']:
if var in os.environ:
del os.environ[var]
print("=== Testing PostHog Fix ===\n")
# Test PostHog directly first
print("1. Testing PostHog directly:")
try:
from posthog import Posthog
# Create PostHog client
ph = Posthog(
project_api_key='phc_skZpl3eFLQJ4iYjsERNMbCO6jfeSJi2vyZlPahKgxZ7',
host='https://eu.i.posthog.com'
)
# Send test event
ph.capture('test-user', 'direct_test', {'timestamp': time.time()})
# Flush and shutdown properly
ph.flush()
ph.shutdown()
print("✓ Direct PostHog test successful\n")
except Exception as e:
print(f"✗ Direct PostHog test failed: {e}\n")
# Test telemetry module
print("2. Testing telemetry module:")
from praisonaiagents.telemetry.telemetry import MinimalTelemetry
# Create telemetry instance
telemetry = MinimalTelemetry(enabled=True)
# Track events
telemetry.track_agent_execution("TestAgent", success=True)
telemetry.track_task_completion("TestTask", success=True)
telemetry.track_tool_usage("TestTool", success=True)
print(f"✓ Events tracked")
print(f"✓ PostHog client available: {telemetry._posthog is not None}")
# Flush
print("\n3. Flushing telemetry...")
telemetry.flush()
print("✓ Flush completed")
# Shutdown
print("\n4. Shutting down telemetry...")
telemetry.shutdown()
print("✓ Shutdown completed")
print("\n=== Test Complete ===")
print("\nPostHog should now be receiving data properly!")
print("The fix adds:")
print("1. posthog.flush() call in the flush() method")
print("2. shutdown() method that properly closes the connection")
+88 -96
View File
@@ -585,19 +585,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/9d/37e5da7519de7b0b070a3fedd4230fe76d50d2a21403e0f2153d70ac4163/cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", size = 3128774 },
]
[[package]]
name = "dataclasses-json"
version = "0.6.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "marshmallow" },
{ name = "typing-inspect" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686 },
]
[[package]]
name = "defusedxml"
version = "0.7.1"
@@ -1197,6 +1184,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/91/61/c80ef80ed8a0a21158e289ef70dac01e351d929a1c30cb0f49be60772547/jiter-0.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:3ac9f578c46f22405ff7f8b1f5848fb753cc4b8377fbec8470a7dc3997ca7566", size = 202374 },
]
[[package]]
name = "json-repair"
version = "0.39.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/60/6d1599bc01070d9fe3840d245ae80fd24b981c732d962842825ce7a9fde6/json_repair-0.39.1.tar.gz", hash = "sha256:e90a489f247e1a8fc86612a5c719872a3dbf9cbaffd6d55f238ec571a77740fa", size = 30040 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/b9/2e445481555422b907dab468b53574bc1e995099ca1a1201d0d876ca05e9/json_repair-0.39.1-py3-none-any.whl", hash = "sha256:3001409a2f319249f13e13d6c622117a5b70ea7e0c6f43864a0233cdffc3a599", size = 20686 },
]
[[package]]
name = "jsonpatch"
version = "1.33"
@@ -1286,29 +1282,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/5c/5c0be747261e1f8129b875fa3bfea736bc5fe17652f9d5e15ca118571b6f/langchain-0.3.25-py3-none-any.whl", hash = "sha256:931f7d2d1eaf182f9f41c5e3272859cfe7f94fc1f7cef6b3e5a46024b4884c21", size = 1011008 },
]
[[package]]
name = "langchain-community"
version = "0.3.24"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "dataclasses-json" },
{ name = "httpx-sse" },
{ name = "langchain" },
{ name = "langchain-core" },
{ name = "langsmith" },
{ name = "numpy" },
{ name = "pydantic-settings" },
{ name = "pyyaml" },
{ name = "requests" },
{ name = "sqlalchemy" },
{ name = "tenacity" },
]
sdist = { url = "https://files.pythonhosted.org/packages/01/f6/4892d1f1cf6d3e89da6ee6cfb0eb82b908c706c58bde7df28367ee76a93f/langchain_community-0.3.24.tar.gz", hash = "sha256:62d9e8cf9aadf35182ec3925f9ec1c8e5e84fb4f199f67a01aee496d289dc264", size = 33233643 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/cb/582f22d74d69f4dbd41e98d361ee36922b79a245a9411383327bd4b63747/langchain_community-0.3.24-py3-none-any.whl", hash = "sha256:b6cdb376bf1c2f4d2503aca20f8f35f2d5b3d879c52848277f20ce1950e7afaf", size = 2528335 },
]
[[package]]
name = "langchain-core"
version = "0.3.63"
@@ -1327,6 +1300,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/71/a748861e6a69ab6ef50ab8e65120422a1f36245c71a0dd0f02de49c208e1/langchain_core-0.3.63-py3-none-any.whl", hash = "sha256:f91db8221b1bc6808f70b2e72fded1a94d50ee3f1dff1636fb5a5a514c64b7f5", size = 438468 },
]
[[package]]
name = "langchain-neo4j"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain" },
{ name = "langchain-core" },
{ name = "neo4j" },
{ name = "neo4j-graphrag" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f9/29/b1c485eaf5adffc59d97d2a47e9b5f3fcfc58becb0e92ded197b98c03138/langchain_neo4j-0.4.0.tar.gz", hash = "sha256:3f059a66411cec1062a2b8c44953a70d0fff9e123e9fb1d6b3f17a0bef6d6114", size = 27061 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/70/7e/d782a77ed3f561a466410aa384f93f59e33930e3d64686d0e1dac4d598c5/langchain_neo4j-0.4.0-py3-none-any.whl", hash = "sha256:2760b5757e7a402884cf3419830217651df97fe4f44b3fec6c96b14b6d7fd18e", size = 31349 },
]
[[package]]
name = "langchain-text-splitters"
version = "0.3.8"
@@ -1359,7 +1347,7 @@ wheels = [
[[package]]
name = "litellm"
version = "1.59.1"
version = "1.72.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -1374,9 +1362,9 @@ dependencies = [
{ name = "tiktoken" },
{ name = "tokenizers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/92/98/80766e2820e140b4b03a4b3e997d25e522bc2ce1d3c79c98a7b856a84e1f/litellm-1.59.1.tar.gz", hash = "sha256:ca4fd7789cc493daa63fe0dd69e3351237708c48a9305ccbc685a1bcf9eecf98", size = 6374267 }
sdist = { url = "https://files.pythonhosted.org/packages/d0/a7/67da5f515c3847bd121b5fde54ac48ee3e7d55d75c49d2b51a2f9b20ab7a/litellm-1.72.1.tar.gz", hash = "sha256:d5d822988d62db8fe434aca7584e1cf8f5a03c033e957e3b5c1ca24366840951", size = 8095871 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/3c/d2edad7c3cc85d83a4a6929c80ee258ae8875e2366366b9ab93eaf817cb3/litellm-1.59.1-py3-none-any.whl", hash = "sha256:69adf83f6942b9b62d3398cd8c1dc2e53b02e9b97bc5c45c427ed917629048e7", size = 6661803 },
{ url = "https://files.pythonhosted.org/packages/10/2d/48dee9e0cc782a74d7361a3a1acf70d9ac8a9278a4fa8d2dcf2ac7359ed8/litellm-1.72.1-py3-none-any.whl", hash = "sha256:858b4458f199dd8d3acd46436cf5fb1de19404e5dcf92ff59ec417c224b52481", size = 7994774 },
]
[[package]]
@@ -1608,18 +1596,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
]
[[package]]
name = "marshmallow"
version = "3.26.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878 },
]
[[package]]
name = "mcp"
version = "1.6.0"
@@ -1650,7 +1626,7 @@ wheels = [
[[package]]
name = "mem0ai"
version = "0.1.44"
version = "0.1.104"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "openai" },
@@ -1660,14 +1636,14 @@ dependencies = [
{ name = "qdrant-client" },
{ name = "sqlalchemy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/81/e3/46e695de8b7d1dc3f922425b01797c1bd3ee63bde449f22d0f1f10bd9df9/mem0ai-0.1.44.tar.gz", hash = "sha256:93214272915d94f673d370bb8fe7a8bfc21806267e65700b471bec454dcdfa5c", size = 63273 }
sdist = { url = "https://files.pythonhosted.org/packages/49/d3/d2bbab0e505be71794bb5d53a67c740cf0ed8187d3965d68e317d7f3b208/mem0ai-0.1.104.tar.gz", hash = "sha256:4193bc2a2d5e9e299f3efb5ad80a9f3296e343b20e24cf2326c1ac7efdcfc773", size = 100245 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/37/96382bf64375237d1fed98813d19aba725e8089e1044b7a0873ddf74576b/mem0ai-0.1.44-py3-none-any.whl", hash = "sha256:32260a2cd935035a1b16ce04ad2e4510a5bd97618709466e2d06303e0eb8d9d4", size = 89595 },
{ url = "https://files.pythonhosted.org/packages/da/03/818b01ad786aeb719ca73408c9ceae9e8193bc1a6aaba86c11c8fdd002b8/mem0ai-0.1.104-py3-none-any.whl", hash = "sha256:7c1f7dd11dcfa8ce827a2911a9dceee98ec8e149a5454e3f8673504832a9186d", size = 156132 },
]
[package.optional-dependencies]
graph = [
{ name = "langchain-community" },
{ name = "langchain-neo4j" },
{ name = "neo4j" },
{ name = "rank-bm25" },
]
@@ -1744,15 +1720,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/d3/f7e6d7d062b8d7072c3989a528d9d47486ee5d5ae75250f6e26b4976d098/mmh3-5.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:122fa9ec148383f9124292962bda745f192b47bfd470b2af5fe7bb3982b17896", size = 36539 },
]
[[package]]
name = "monotonic"
version = "1.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ea/ca/8e91948b782ddfbd194f323e7e7d9ba12e5877addf04fb2bf8fca38e86ac/monotonic-1.6.tar.gz", hash = "sha256:3a55207bcfed53ddd5c5bae174524062935efed17792e9de2ad0205ce9ad63f7", size = 7615 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/67/7e8406a29b6c45be7af7740456f7f37025f0506ae2e05fb9009a53946860/monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c", size = 8154 },
]
[[package]]
name = "mpmath"
version = "1.3.0"
@@ -1860,15 +1827,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 },
]
[[package]]
name = "neo4j"
version = "5.28.1"
@@ -1881,6 +1839,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/57/94225fe5e9dabdc0ff60c88cbfcedf11277f4b34e7ab1373d3e62dbdd207/neo4j-5.28.1-py3-none-any.whl", hash = "sha256:6755ef9e5f4e14b403aef1138fb6315b120631a0075c138b5ddb2a06b87b09fd", size = 312258 },
]
[[package]]
name = "neo4j-graphrag"
version = "1.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "fsspec" },
{ name = "json-repair" },
{ name = "neo4j" },
{ name = "pydantic" },
{ name = "pypdf" },
{ name = "pyyaml" },
{ name = "types-pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7c/5a/b9309d488b7cfe63326b6502b12dfae16558d64ead4d007b5e6392dd36ac/neo4j_graphrag-1.7.0.tar.gz", hash = "sha256:c6c1f730e680d8af3ff3e1ee6086c465c15dd3245157e6c40606ac3ddae7d4f0", size = 104306 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/2a/b9e29d7a1068302f94dc05d682f1068795947f6c0a99d8274e7f1ca29d23/neo4j_graphrag-1.7.0-py3-none-any.whl", hash = "sha256:29a854f2f1e268f043446cdd387c72ee954b87726329ef6479c59ed7b9cf0751", size = 180365 },
]
[[package]]
name = "numpy"
version = "2.2.1"
@@ -1999,7 +1975,7 @@ wheels = [
[[package]]
name = "openai"
version = "1.58.1"
version = "1.84.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -2011,9 +1987,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/27/3c/b1ecce430ed56fa3ac1b0676966d3250aab9c70a408232b71e419ea62148/openai-1.58.1.tar.gz", hash = "sha256:f5a035fd01e141fc743f4b0e02c41ca49be8fab0866d3b67f5f29b4f4d3c0973", size = 343411 }
sdist = { url = "https://files.pythonhosted.org/packages/91/a3/128caf24e116f48fad3e4d5122cdf84db06c5127911849d51663c66158c8/openai-1.84.0.tar.gz", hash = "sha256:4caa43bdab262cc75680ce1a2322cfc01626204074f7e8d9939ab372acf61698", size = 467066 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8e/5a/d22cd07f1a99b9e8b3c92ee0c1959188db4318828a3d88c9daac120bdd69/openai-1.58.1-py3-none-any.whl", hash = "sha256:e2910b1170a6b7f88ef491ac3a42c387f08bd3db533411f7ee391d166571d63c", size = 454279 },
{ url = "https://files.pythonhosted.org/packages/2a/10/f245db006a860dbc1f2e2c8382e0a1762c7753e7971ba43a1dc3f3ec1404/openai-1.84.0-py3-none-any.whl", hash = "sha256:7ec4436c3c933d68dc0f5a0cef0cb3dbc0864a54d62bddaf2ed5f3d521844711", size = 725512 },
]
[[package]]
@@ -2382,27 +2358,28 @@ wheels = [
[[package]]
name = "posthog"
version = "3.7.5"
version = "4.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backoff" },
{ name = "monotonic" },
{ name = "distro" },
{ name = "python-dateutil" },
{ name = "requests" },
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/e9/1cd7492bb58dd255129467e1221e2d6f51aa0c6f3c781ac9ac29cc8a2859/posthog-3.7.5.tar.gz", hash = "sha256:8ba40ab623da35db72715fc87fe7dccb7fc272ced92581fe31db2d4dbe7ad761", size = 50269 }
sdist = { url = "https://files.pythonhosted.org/packages/89/df/cb1b1837a38dfde0eca9b7947037ef0abf9e7732a7c29b8eab6cd4af1a0b/posthog-4.3.2.tar.gz", hash = "sha256:1dc422ccef560ee0949b00e340e8135160fd71a4eb03f06ec382c4672ab0fe4c", size = 85399 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/bd/2d550ac79443cdbb6a5a4193c37820f04df0499e1ecbe8e41c5462cf0c2d/posthog-3.7.5-py2.py3-none-any.whl", hash = "sha256:022132c17069dde03c5c5904e2ae1b9bd68d5059cbc5a8dffc5c1537a1b71cb5", size = 54882 },
{ url = "https://files.pythonhosted.org/packages/2e/e7/2983efeec4356cbae0fcd775effc76576377ae86330d25c150a5bbf8fa4c/posthog-4.3.2-py2.py3-none-any.whl", hash = "sha256:e53d0e20cc8d4ec9cb62f0cbf6034e8f93e7f5b3fc502dfcc8813feb12a06bb7", size = 102036 },
]
[[package]]
name = "praisonaiagents"
version = "0.0.102"
version = "0.0.106"
source = { editable = "." }
dependencies = [
{ name = "mcp" },
{ name = "openai" },
{ name = "posthog" },
{ name = "pydantic" },
{ name = "rich" },
]
@@ -2416,6 +2393,7 @@ all = [
{ name = "markitdown", extra = ["all"] },
{ name = "mcp" },
{ name = "mem0ai", extra = ["graph"] },
{ name = "posthog" },
{ name = "pydantic" },
{ name = "uvicorn" },
]
@@ -2446,6 +2424,9 @@ memory = [
{ name = "chromadb" },
{ name = "litellm" },
]
telemetry = [
{ name = "posthog" },
]
[package.metadata]
requires-dist = [
@@ -2455,20 +2436,23 @@ requires-dist = [
{ name = "chromadb", marker = "extra == 'memory'", specifier = ">=1.0.0" },
{ name = "fastapi", marker = "extra == 'api'", specifier = ">=0.115.0" },
{ name = "fastapi", marker = "extra == 'mcp'", specifier = ">=0.115.0" },
{ name = "litellm", marker = "extra == 'llm'", specifier = ">=1.50.0" },
{ name = "litellm", marker = "extra == 'memory'", specifier = ">=1.50.0" },
{ name = "litellm", marker = "extra == 'llm'", specifier = ">=1.72.0" },
{ name = "litellm", marker = "extra == 'memory'", specifier = ">=1.72.0" },
{ name = "markitdown", extras = ["all"], marker = "extra == 'knowledge'", specifier = ">=0.1.0" },
{ name = "mcp", specifier = ">=1.6.0" },
{ name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.6.0" },
{ name = "mem0ai", marker = "extra == 'knowledge'", specifier = ">=0.1.0" },
{ name = "mem0ai", extras = ["graph"], marker = "extra == 'graph'", specifier = ">=0.1.0" },
{ name = "openai" },
{ name = "posthog", specifier = ">=3.0.0" },
{ name = "posthog", marker = "extra == 'telemetry'", specifier = ">=3.0.0" },
{ name = "praisonaiagents", extras = ["api"], marker = "extra == 'all'" },
{ name = "praisonaiagents", extras = ["graph"], marker = "extra == 'all'" },
{ name = "praisonaiagents", extras = ["knowledge"], marker = "extra == 'all'" },
{ name = "praisonaiagents", extras = ["llm"], marker = "extra == 'all'" },
{ name = "praisonaiagents", extras = ["mcp"], marker = "extra == 'all'" },
{ name = "praisonaiagents", extras = ["memory"], marker = "extra == 'all'" },
{ name = "praisonaiagents", extras = ["telemetry"], marker = "extra == 'all'" },
{ name = "pydantic" },
{ name = "pydantic", marker = "extra == 'llm'", specifier = ">=2.4.2" },
{ name = "rich" },
@@ -2727,6 +2711,18 @@ crypto = [
{ name = "cryptography" },
]
[[package]]
name = "pypdf"
version = "5.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/40/46/67de1d7a65412aa1c896e6b280829b70b57d203fadae6859b690006b8e0a/pypdf-5.6.0.tar.gz", hash = "sha256:a4b6538b77fc796622000db7127e4e58039ec5e6afd292f8e9bf42e2e985a749", size = 5023749 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/8b/dc3a72d98c22be7a4cbd664ad14c5a3e6295c2dbdf572865ed61e24b5e38/pypdf-5.6.0-py3-none-any.whl", hash = "sha256:ca6bf446bfb0a2d8d71d6d6bb860798d864c36a29b3d9ae8d7fc7958c59f88e7", size = 304208 },
]
[[package]]
name = "pypika"
version = "0.48.9"
@@ -3463,6 +3459,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 },
]
[[package]]
name = "types-pyyaml"
version = "6.0.12.20250516"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/22/59e2aeb48ceeee1f7cd4537db9568df80d62bdb44a7f9e743502ea8aab9c/types_pyyaml-6.0.12.20250516.tar.gz", hash = "sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba", size = 17378 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/5f/e0af6f7f6a260d9af67e1db4f54d732abad514252a7a378a6c4d17dd1036/types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530", size = 20312 },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
@@ -3472,19 +3477,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]
name = "typing-inspect"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827 },
]
[[package]]
name = "tzdata"
version = "2024.2"
+13 -3
View File
@@ -1,6 +1,16 @@
OPENAI_MODEL_NAME="gpt-4o"
# OpenAI / compatible API
OPENAI_API_KEY="Enter your API key"
OPENAI_MODEL_NAME="gpt-4o"
OPENAI_API_BASE="https://api.openai.com/v1"
# AG2 framework (uses same OPENAI_* vars above, or override below)
# MODEL_NAME=gpt-4o-mini
# AWS Bedrock (for ag2_bedrock.yaml example)
# AWS_DEFAULT_REGION=us-east-1
# AWS_ACCESS_KEY_ID=your-access-key
# AWS_SECRET_ACCESS_KEY=your-secret-key
# Chainlit (optional)
CHAINLIT_USERNAME=admin
CHAINLIT_USERNAME=admin
CHAINLIT_AUTH_SECRET="chainlit create-secret to create"
CHAINLIT_AUTH_SECRET="chainlit create-secret to create"
+2 -2
View File
@@ -3,8 +3,8 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.30.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.30.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.33.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.33.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+164 -2
View File
@@ -41,6 +41,16 @@ try:
except ImportError:
pass
AG2_AVAILABLE = False
try:
import importlib.metadata as _importlib_metadata
_importlib_metadata.distribution('ag2')
from autogen import LLMConfig as _AG2LLMConfig # noqa: F401 — AG2-exclusive class
AG2_AVAILABLE = True
del _AG2LLMConfig, _importlib_metadata
except Exception:
pass
try:
import agentops
AGENTOPS_AVAILABLE = True
@@ -51,7 +61,7 @@ except ImportError:
pass
# Only try to import praisonai_tools if either CrewAI or AutoGen is available
if CREWAI_AVAILABLE or AUTOGEN_AVAILABLE or PRAISONAI_AVAILABLE:
if CREWAI_AVAILABLE or AUTOGEN_AVAILABLE or PRAISONAI_AVAILABLE or AG2_AVAILABLE:
try:
from praisonai_tools import (
CodeDocsSearchTool, CSVSearchTool, DirectorySearchTool, DOCXSearchTool, DirectoryReadTool,
@@ -127,6 +137,8 @@ class AgentsGenerator:
raise ImportError("AutoGen is not installed. Please install it with 'pip install praisonai[autogen]'")
elif framework == "praisonai" and not PRAISONAI_AVAILABLE:
raise ImportError("PraisonAI is not installed. Please install it with 'pip install praisonaiagents'")
elif framework == "ag2" and not AG2_AVAILABLE:
raise ImportError("AG2 is not installed. Please install it with 'pip install praisonai[ag2]'")
def is_function_or_decorated(self, obj):
"""
@@ -274,7 +286,7 @@ class AgentsGenerator:
tools_dict = {}
# Only try to use praisonai_tools if it's available and needed
if PRAISONAI_TOOLS_AVAILABLE and (CREWAI_AVAILABLE or AUTOGEN_AVAILABLE or PRAISONAI_AVAILABLE):
if PRAISONAI_TOOLS_AVAILABLE and (CREWAI_AVAILABLE or AUTOGEN_AVAILABLE or PRAISONAI_AVAILABLE or AG2_AVAILABLE):
tools_dict = {
'CodeDocsSearchTool': CodeDocsSearchTool(),
'CSVSearchTool': CSVSearchTool(),
@@ -327,6 +339,12 @@ class AgentsGenerator:
if AGENTOPS_AVAILABLE:
agentops.init(os.environ.get("AGENTOPS_API_KEY"), default_tags=["praisonai"])
return self._run_praisonai(config, topic, tools_dict)
elif framework == "ag2":
if not AG2_AVAILABLE:
raise ImportError("AG2 is not installed. Please install it with 'pip install praisonai[ag2]'")
if AGENTOPS_AVAILABLE:
agentops.init(os.environ.get("AGENTOPS_API_KEY"), default_tags=["ag2"])
return self._run_ag2(config, topic, tools_dict)
else: # framework=crewai
if not CREWAI_AVAILABLE:
raise ImportError("CrewAI is not installed. Please install it with 'pip install praisonai[crewai]'")
@@ -407,6 +425,150 @@ class AgentsGenerator:
return result
def _run_ag2(self, config, topic, tools_dict):
"""
Run agents using the AG2 framework (community fork of AutoGen, PyPI: ag2).
AG2 installs under the 'autogen' namespace — there is no 'import ag2'.
Uses LLMConfig context manager + AssistantAgent + GroupChat pattern.
Args:
config (dict): Configuration dictionary parsed from YAML
topic (str): The topic/task to process
tools_dict (dict): Dictionary of available tools
Returns:
str: Result prefixed with '### AG2 Output ###'
"""
import re
from autogen import (
AssistantAgent, UserProxyAgent, GroupChat, GroupChatManager, LLMConfig
)
model_config = self.config_list[0] if self.config_list else {}
# Allow YAML top-level llm block to override config_list values
yaml_llm = config.get("llm", {}) or {}
# Also check first role's llm block as a fallback
first_role_llm = {}
for role_details in config.get("roles", {}).values():
first_role_llm = role_details.get("llm", {}) or {}
break
# Priority: YAML top-level llm > first role llm > config_list > env vars
def _resolve(key, env_var=None, default=None):
return (yaml_llm.get(key) or first_role_llm.get(key)
or model_config.get(key)
or (os.environ.get(env_var) if env_var else None)
or default)
api_type = _resolve("api_type", default="openai").lower()
model_name = _resolve("model", default="gpt-4o-mini")
api_key = _resolve("api_key", env_var="OPENAI_API_KEY")
# Fix #3: also check OPENAI_API_BASE for consistency with rest of codebase
base_url = (model_config.get("base_url")
or yaml_llm.get("base_url")
or os.environ.get("OPENAI_BASE_URL")
or os.environ.get("OPENAI_API_BASE"))
# Build LLMConfig — pass a config dict; Bedrock needs no api_key
if api_type == "bedrock":
llm_config_entry = {"api_type": "bedrock", "model": model_name}
else:
llm_config_entry = {"model": model_name}
if api_key:
llm_config_entry["api_key"] = api_key
if base_url and base_url not in ("https://api.openai.com/v1", "https://api.openai.com/v1/"):
llm_config_entry["base_url"] = base_url
llm_config = LLMConfig(llm_config_entry)
user_proxy = UserProxyAgent(
name="User",
human_input_mode="NEVER",
is_termination_msg=lambda x: "TERMINATE" in (x.get("content") or ""),
code_execution_config=False,
)
# Create one AssistantAgent per role, passing llm_config directly
ag2_agent_entries = []
for role, details in config["roles"].items():
agent_name = details.get("role", role).replace("{topic}", topic)
backstory = details.get("backstory", "").replace("{topic}", topic)
agent_name_safe = re.sub(r"[^a-zA-Z0-9_\-]", "_", agent_name)
assistant = AssistantAgent(
name=agent_name_safe,
system_message=backstory + "\nWhen the task is done, reply 'TERMINATE'.",
llm_config=llm_config,
)
ag2_agent_entries.append((role, details, assistant))
# Register tools via AG2 decorator pattern
for role, details, assistant in ag2_agent_entries:
for tool_name in details.get("tools", []):
tool = tools_dict.get(tool_name)
if tool is None:
continue
func = tool if callable(tool) else getattr(tool, "run", None)
if func is None:
continue
def make_tool_fn(f):
def tool_fn(**kwargs):
return f(**kwargs) if callable(f) else str(f)
tool_fn.__name__ = tool_name
return tool_fn
wrapped = make_tool_fn(func)
assistant.register_for_llm(description=f"Tool: {tool_name}")(wrapped)
user_proxy.register_for_execution()(wrapped)
all_assistants = [a for _, _, a in ag2_agent_entries]
if not all_assistants:
return "### AG2 Output ###\nNo agents created from configuration."
# Build initial message from all task descriptions
task_lines = []
for role, details, _ in ag2_agent_entries:
for task_name, task_details in details.get("tasks", {}).items():
desc = task_details.get("description", "").replace("{topic}", topic)
if desc:
task_lines.append(desc)
initial_message = "\n".join(task_lines) if task_lines else topic
groupchat = GroupChat(
agents=[user_proxy] + all_assistants,
messages=[],
max_round=12,
)
manager = GroupChatManager(groupchat=groupchat, llm_config=llm_config)
try:
chat_result = user_proxy.initiate_chat(manager, message=initial_message)
except Exception as e:
return f"### AG2 Error ###\n{str(e)}"
# Prefer ChatResult.summary if available, otherwise scan messages
result_content = ""
summary = getattr(chat_result, "summary", None)
if summary and isinstance(summary, str) and summary.strip():
result_content = re.sub(r'[\s\.\,]*TERMINATE[\s\.\,]*$', '', summary, flags=re.IGNORECASE).strip().rstrip('.')
if not result_content:
for msg in reversed(groupchat.messages):
# Skip the initial user proxy message
if msg.get("name") == "User":
continue
content = (msg.get("content") or "").strip()
if content:
result_content = re.sub(r'[\s\.\,]*TERMINATE[\s\.\,]*$', '', content, flags=re.IGNORECASE).strip().rstrip('.')
if result_content:
break
if not result_content:
result_content = "Task completed."
return f"### AG2 Output ###\n{result_content}"
def _run_crewai(self, config, topic, tools_dict):
"""
Run agents using the CrewAI framework.
+15
View File
@@ -32,6 +32,16 @@ try:
except ImportError:
pass
AG2_AVAILABLE = False
try:
import importlib.metadata as _importlib_metadata
_importlib_metadata.distribution('ag2')
from autogen import LLMConfig as _AG2LLMConfig # noqa: F401 — AG2-exclusive class
AG2_AVAILABLE = True
del _AG2LLMConfig, _importlib_metadata
except Exception:
pass
try:
from praisonai_tools import (
CodeDocsSearchTool, CSVSearchTool, DirectorySearchTool, DOCXSearchTool,
@@ -83,6 +93,11 @@ AutoGen is not installed. Please install with:
Praisonai is not installed. Please install with:
pip install praisonaiagents
""")
elif framework == "ag2" and not AG2_AVAILABLE:
raise ImportError("""
AG2 is not installed. Please install with:
pip install "praisonai[ag2]"
""")
# Only show tools message if using a framework and tools are needed
if (framework in ["crewai", "autogen"]) and not PRAISONAI_TOOLS_AVAILABLE:
+2 -2
View File
@@ -24,8 +24,8 @@ config_list = [
agent_file = "test.yaml"
actions=[
cl.Action(name="run", payload="run", label="✅ Run"),
cl.Action(name="modify", payload="modify", label="🔧 Modify"),
cl.Action(name="run", payload={"value": "run"}, label="run"),
cl.Action(name="modify", payload={"value": "modify"}, label="Modify"),
]
@cl.action_callback("run")
+5
View File
@@ -529,6 +529,7 @@ class PraisonAI:
parser.add_argument("--call", action="store_true", help="Start the PraisonAI Call server")
parser.add_argument("--public", action="store_true", help="Use ngrok to expose the server publicly (only with --call)")
parser.add_argument("--merge", action="store_true", help="Merge existing agents.yaml with auto-generated agents instead of overwriting")
parser.add_argument("--claudecode", action="store_true", help="Enable Claude Code integration for file modifications and coding tasks")
# If we're in a test environment, parse with empty args to avoid pytest interference
if in_test_env:
@@ -549,6 +550,10 @@ class PraisonAI:
if args.command == 'code':
args.ui = 'chainlit'
args.code = True
# Handle --claudecode flag for code command
if getattr(args, 'claudecode', False):
os.environ["PRAISONAI_CLAUDECODE_ENABLED"] = "true"
if args.command == 'realtime':
args.realtime = True
if args.command == 'call':
+1 -1
View File
@@ -56,7 +56,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==2.2.30 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.2.33 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+3 -3
View File
@@ -17,8 +17,8 @@ config_list = [
]
actions = [
cl.Action(name="run", payload="run", label="✅ Run"),
cl.Action(name="modify", payload="modify", label="🔧 Modify"),
cl.Action(name="run", payload={"value": "run"}, label="run"),
cl.Action(name="modify", payload={"value": "modify"}, label="Modify"),
]
@cl.action_callback("run")
@@ -819,4 +819,4 @@ async def on_settings_update(settings):
await on_settings_update(settings)
except Exception as e:
logger.error(f"Final retry failed: {str(e)}")
await cl.Message(content=f"Failed to update settings after retries: {str(e)}").send()
await cl.Message(content=f"Failed to update settings after retries: {str(e)}").send()
+244 -12
View File
@@ -13,16 +13,25 @@ from PIL import Image
from context import ContextGatherer
from tavily import TavilyClient
from crawl4ai import AsyncWebCrawler
import subprocess
# Local application/library imports
import chainlit as cl
from chainlit.input_widget import TextInput
from chainlit.input_widget import TextInput, Switch
from chainlit.types import ThreadDict
import chainlit.data as cl_data
from litellm import acompletion
import litellm
from db import DatabaseManager
# PraisonAI Agents imports
try:
from praisonaiagents import Agent
PRAISONAI_AGENTS_AVAILABLE = True
except ImportError:
PRAISONAI_AGENTS_AVAILABLE = False
# Fallback to litellm for backward compatibility
from litellm import acompletion
import litellm
# Load environment variables
load_dotenv()
@@ -41,14 +50,131 @@ logger.addHandler(console_handler)
# Set the logging level for the logger
logger.setLevel(log_level)
# Configure litellm same as in llm.py
litellm.set_verbose = False
litellm.success_callback = []
litellm._async_success_callback = []
litellm.callbacks = []
litellm.drop_params = True
litellm.modify_params = True
litellm.suppress_debug_messages = True
# Configure litellm for backward compatibility (only if praisonaiagents not available)
if not PRAISONAI_AGENTS_AVAILABLE:
import litellm
litellm.set_verbose = False
litellm.success_callback = []
litellm._async_success_callback = []
litellm.callbacks = []
litellm.drop_params = True
litellm.modify_params = True
litellm.suppress_debug_messages = True
# Claude Code Tool Function
async def claude_code_tool(query: str) -> str:
"""
Execute Claude Code CLI commands for file modifications and coding tasks.
Args:
query: The user's request that requires file modifications or coding assistance
Returns:
The output from Claude Code execution
"""
try:
# Check if the current working directory is a git repository
repo_path = os.environ.get("PRAISONAI_CODE_REPO_PATH", ".")
# Try to detect if git is available and if we're in a git repo
git_available = False
try:
subprocess.run(["git", "status"], cwd=repo_path, capture_output=True, check=True)
git_available = True
except (subprocess.CalledProcessError, FileNotFoundError):
git_available = False
# Build Claude Code command
claude_cmd = ["claude", "--dangerously-skip-permissions", "-p", query]
# Check if it's a continuation (simple heuristic)
user_session_context = cl.user_session.get("claude_code_context", False)
if user_session_context:
claude_cmd.insert(1, "--continue")
# Execute Claude Code command
result = subprocess.run(
claude_cmd,
cwd=repo_path,
capture_output=True,
text=True,
timeout=300 # 5 minutes timeout
)
# Set context for future requests
cl.user_session.set("claude_code_context", True)
output = result.stdout
if result.stderr:
output += f"\n\nErrors:\n{result.stderr}"
# If git is available and changes were made, try to create a branch and PR
if git_available and result.returncode == 0:
try:
# Check for changes
git_status = subprocess.run(
["git", "status", "--porcelain"],
cwd=repo_path,
capture_output=True,
text=True,
check=True
)
if git_status.stdout.strip():
# Create a branch for the changes
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
branch_name = f"claude-code-{timestamp}"
# Create and switch to new branch
subprocess.run(["git", "checkout", "-b", branch_name], cwd=repo_path, check=True)
# Add and commit changes
subprocess.run(["git", "add", "."], cwd=repo_path, check=True)
commit_message = f"Claude Code changes: {query[:50]}..."
subprocess.run(
["git", "commit", "-m", commit_message],
cwd=repo_path,
check=True
)
# Push to remote (if configured)
try:
subprocess.run(
["git", "push", "-u", "origin", branch_name],
cwd=repo_path,
check=True
)
# Generate PR URL (assuming GitHub)
remote_url = subprocess.run(
["git", "config", "--get", "remote.origin.url"],
cwd=repo_path,
capture_output=True,
text=True
)
if remote_url.returncode == 0:
repo_url = remote_url.stdout.strip()
if repo_url.endswith(".git"):
repo_url = repo_url[:-4]
if "github.com" in repo_url:
pr_url = f"{repo_url}/compare/main...{branch_name}?quick_pull=1"
output += f"\n\n📋 **Pull Request Created:**\n{pr_url}"
except subprocess.CalledProcessError:
output += f"\n\n🌲 **Branch created:** {branch_name} (push manually if needed)"
except subprocess.CalledProcessError as e:
output += f"\n\nGit operations failed: {e}"
return output
except subprocess.TimeoutExpired:
return "Claude Code execution timed out after 5 minutes."
except subprocess.CalledProcessError as e:
return f"Claude Code execution failed: {e}\nStdout: {e.stdout}\nStderr: {e.stderr}"
except Exception as e:
return f"Error executing Claude Code: {str(e)}"
CHAINLIT_AUTH_SECRET = os.getenv("CHAINLIT_AUTH_SECRET")
@@ -109,6 +235,12 @@ async def start():
model_name = os.getenv("MODEL_NAME", "gpt-4o-mini")
cl.user_session.set("model_name", model_name)
logger.debug(f"Model name: {model_name}")
# Load Claude Code setting (check CLI flag first, then database setting)
claude_code_enabled = os.getenv("PRAISONAI_CLAUDECODE_ENABLED", "false").lower() == "true"
if not claude_code_enabled:
claude_code_enabled = (load_setting("claude_code_enabled") or "false").lower() == "true"
settings = cl.ChatSettings(
[
TextInput(
@@ -116,6 +248,11 @@ async def start():
label="Enter the Model Name",
placeholder="e.g., gpt-4o-mini",
initial=model_name
),
Switch(
id="claude_code_enabled",
label="Enable Claude Code (file modifications & coding)",
initial=claude_code_enabled
)
]
)
@@ -134,10 +271,13 @@ async def setup_agent(settings):
logger.debug(settings)
cl.user_session.set("settings", settings)
model_name = settings["model_name"]
claude_code_enabled = settings.get("claude_code_enabled", False)
cl.user_session.set("model_name", model_name)
cl.user_session.set("claude_code_enabled", claude_code_enabled)
# Save in settings table
save_setting("model_name", model_name)
save_setting("claude_code_enabled", str(claude_code_enabled).lower())
# Save in thread metadata
thread_id = cl.user_session.get("thread_id")
@@ -152,6 +292,7 @@ async def setup_agent(settings):
metadata = {}
metadata["model_name"] = model_name
metadata["claude_code_enabled"] = claude_code_enabled
# Always store metadata as a dictionary
await cl_data._data_layer.update_thread(thread_id, metadata=metadata)
@@ -221,6 +362,7 @@ tools = [{
@cl.on_message
async def main(message: cl.Message):
model_name = load_setting("model_name") or os.getenv("MODEL_NAME") or "gpt-4o-mini"
claude_code_enabled = cl.user_session.get("claude_code_enabled", False)
message_history = cl.user_session.get("message_history", [])
repo_path_to_use = os.environ.get("PRAISONAI_CODE_REPO_PATH", ".")
gatherer = ContextGatherer(directory=repo_path_to_use)
@@ -258,6 +400,87 @@ Context:
msg = cl.Message(content="")
await msg.send()
# Use PraisonAI Agents if available, otherwise fallback to litellm
if PRAISONAI_AGENTS_AVAILABLE:
await handle_with_praisonai_agents(message, user_message, model_name, claude_code_enabled, msg, image)
else:
await handle_with_litellm(user_message, model_name, message_history, msg, image)
async def handle_with_praisonai_agents(message, user_message, model_name, claude_code_enabled, msg, image):
"""Handle message using PraisonAI Agents framework with optional Claude Code tool"""
try:
# Prepare tools list
available_tools = []
# Add Tavily search tool if API key available
if tavily_api_key:
available_tools.append(tavily_web_search)
# Add Claude Code tool if enabled
if claude_code_enabled:
available_tools.append(claude_code_tool)
# Create agent instructions
instructions = """You are a helpful AI assistant. Use the available tools when needed to provide comprehensive responses.
If Claude Code tool is available and the user's request involves:
- File modifications, code changes, or implementation tasks
- Creating, editing, or debugging code
- Project setup or development tasks
- Git operations or version control
Then use the Claude Code tool to handle those requests.
For informational questions, explanations, or general conversations, respond normally without using Claude Code."""
# Create agent
agent = Agent(
name="PraisonAI Assistant",
instructions=instructions,
llm=model_name,
tools=available_tools if available_tools else None
)
# Execute agent with streaming
full_response = ""
# Use agent's streaming capabilities if available
try:
# For now, use synchronous execution and stream the result
# TODO: Implement proper streaming when PraisonAI agents support it
result = agent.start(user_message)
# Stream the response character by character for better UX
if hasattr(result, 'raw'):
response_text = result.raw
else:
response_text = str(result)
for char in response_text:
await msg.stream_token(char)
full_response += char
# Small delay to make streaming visible
await asyncio.sleep(0.01)
except Exception as e:
error_response = f"Error executing agent: {str(e)}"
for char in error_response:
await msg.stream_token(char)
full_response += char
await asyncio.sleep(0.01)
msg.content = full_response
await msg.update()
except Exception as e:
error_msg = f"Failed to use PraisonAI Agents: {str(e)}"
logger.error(error_msg)
await msg.stream_token(error_msg)
msg.content = error_msg
await msg.update()
async def handle_with_litellm(user_message, model_name, message_history, msg, image):
"""Fallback handler using litellm for backward compatibility"""
# Prepare the completion parameters using the helper function
completion_params = _build_completion_params(
model_name,
@@ -279,7 +502,7 @@ Context:
]
}
# Use a vision-capable model when an image is present
completion_params["model"] = "gpt-4-vision-preview" # Adjust this to your actual vision-capable model
completion_params["model"] = "gpt-4-vision-preview"
# Only add tools and tool_choice if Tavily API key is available and no image is uploaded
if tavily_api_key:
@@ -412,6 +635,10 @@ async def send_count():
async def on_chat_resume(thread: ThreadDict):
logger.info(f"Resuming chat: {thread['id']}")
model_name = load_setting("model_name") or os.getenv("MODEL_NAME") or "gpt-4o-mini"
# Load Claude Code setting (check CLI flag first, then database setting)
claude_code_enabled = os.getenv("PRAISONAI_CLAUDECODE_ENABLED", "false").lower() == "true"
if not claude_code_enabled:
claude_code_enabled = (load_setting("claude_code_enabled") or "false").lower() == "true"
logger.debug(f"Model name: {model_name}")
settings = cl.ChatSettings(
[
@@ -420,6 +647,11 @@ async def on_chat_resume(thread: ThreadDict):
label="Enter the Model Name",
placeholder="e.g., gpt-4o-mini",
initial=model_name
),
Switch(
id="claude_code_enabled",
label="Enable Claude Code (file modifications & coding)",
initial=claude_code_enabled
)
]
)
+7 -4
View File
@@ -1,6 +1,6 @@
[project]
name = "PraisonAI"
version = "2.2.30"
version = "2.2.33"
description = "PraisonAI is an AI Agents Framework with Self Reflection. PraisonAI application combines PraisonAI Agents, AutoGen, and CrewAI into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customisation, and efficient human-agent collaboration."
readme = "README.md"
license = ""
@@ -12,7 +12,7 @@ dependencies = [
"rich>=13.7",
"markdown>=3.5",
"pyparsing>=3.0.0",
"praisonaiagents>=0.0.102",
"praisonaiagents>=0.0.106",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
@@ -92,10 +92,11 @@ call = [
train = []
crewai = ["crewai>=0.32.0", "praisonai-tools>=0.0.15"]
autogen = ["pyautogen>=0.2.19", "praisonai-tools>=0.0.15", "crewai"]
ag2 = ["ag2>=0.11.0", "praisonai-tools>=0.0.15"]
[tool.poetry]
name = "PraisonAI"
version = "2.2.30"
version = "2.2.33"
description = "PraisonAI is an AI Agents Framework with Self Reflection. PraisonAI application combines PraisonAI Agents, AutoGen, and CrewAI into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customisation, and efficient human-agent collaboration."
authors = ["Mervin Praison"]
license = ""
@@ -113,12 +114,13 @@ python = ">=3.10,<3.13"
rich = ">=13.7"
markdown = ">=3.5"
pyparsing = ">=3.0.0"
praisonaiagents = ">=0.0.102"
praisonaiagents = ">=0.0.106"
python-dotenv = ">=0.19.0"
instructor = ">=1.3.3"
PyYAML = ">=6.0"
mcp = ">=1.6.0"
pyautogen = {version = ">=0.2.19", optional = true}
ag2 = {version = ">=0.11.0", optional = true}
crewai = {version = ">=0.32.0", optional = true}
praisonai-tools = {version = ">=0.0.15", optional = true}
chainlit = {version = "==2.5.5", optional = true}
@@ -277,6 +279,7 @@ call = [
]
crewai = ["crewai", "praisonai-tools"]
autogen = ["pyautogen", "praisonai-tools", "crewai"]
ag2 = ["ag2", "praisonai-tools"]
[tool.poetry-dynamic-versioning]
enable = true
@@ -0,0 +1,291 @@
# Claude Code Integration with PraisonAI Agents
This document describes the integration of Claude Code with PraisonAI Agents, allowing AI agents to intelligently decide when to use Claude Code for file modifications and coding tasks.
## Overview
The refactored implementation replaces direct `litellm` usage with `praisonaiagents`, making Claude Code a custom tool that the AI agent can choose to use based on the user's request. This provides better intelligence and flexibility while maintaining backward compatibility.
## Key Features
### 🤖 Agent-Driven Decision Making
- AI agent intelligently decides when to use Claude Code vs. regular responses
- No manual detection logic - the agent understands context
- Automatic tool selection based on request type
### 🔧 Claude Code as a Custom Tool
- Claude Code is implemented as a tool function for PraisonAI Agents
- Executes `claude --dangerously-skip-permissions -p "query"`
- Supports `--continue` flag for conversation continuity
- Git operations with automatic branch creation and PR URLs
### ⚙️ Flexible Configuration
- CLI flag: `praisonai code --claudecode`
- UI toggle: "Enable Claude Code (file modifications & coding)"
- Environment variable: `PRAISONAI_CLAUDECODE_ENABLED=true`
### 🔄 Backward Compatibility
- Falls back to `litellm` if `praisonaiagents` is not available
- Maintains all existing functionality
- No breaking changes to existing workflows
## Architecture
### Agent Instructions
The agent receives these instructions to guide tool usage:
```
You are a helpful AI assistant. Use the available tools when needed to provide comprehensive responses.
If Claude Code tool is available and the user's request involves:
- File modifications, code changes, or implementation tasks
- Creating, editing, or debugging code
- Project setup or development tasks
- Git operations or version control
Then use the Claude Code tool to handle those requests.
For informational questions, explanations, or general conversations, respond normally without using Claude Code.
```
### Tool Function: `claude_code_tool`
```python
async def claude_code_tool(query: str) -> str:
"""
Execute Claude Code CLI commands for file modifications and coding tasks.
Args:
query: The user's request that requires file modifications or coding assistance
Returns:
The output from Claude Code execution
"""
```
**Features:**
- Executes Claude Code with `--dangerously-skip-permissions`
- Supports conversation continuity with `--continue` flag
- Automatic git branch creation and management
- PR URL generation for GitHub repositories
- 5-minute timeout for long operations
- Comprehensive error handling
### Available Tools
When Claude Code is enabled, the agent has access to:
1. **Claude Code Tool** - For file modifications and coding tasks
2. **Tavily Web Search** - For internet research (if API key configured)
The agent decides which tool(s) to use based on the user's request.
## Usage
### Command Line Interface
```bash
# Enable Claude Code integration
praisonai code --claudecode
# Use regular mode (no Claude Code)
praisonai code
```
### Environment Variables
```bash
# Force enable Claude Code (overrides UI setting)
export PRAISONAI_CLAUDECODE_ENABLED=true
# Set repository path (defaults to current directory)
export PRAISONAI_CODE_REPO_PATH=/path/to/your/repo
```
### UI Settings
In the PraisonAI Code UI, you can:
- Toggle "Enable Claude Code (file modifications & coding)" switch
- Setting is saved to database for persistence
- CLI flag takes precedence over UI setting
## Examples
### File Modification Request
**User:** "Create a new Python function to calculate fibonacci numbers"
**Agent Decision:** Uses Claude Code tool → Creates/modifies files → Returns implementation details
### Informational Request
**User:** "How does fibonacci algorithm work?"
**Agent Decision:** Responds normally → Provides explanation without file modifications
### Mixed Request
**User:** "Explain how sorting works and implement quicksort in Python"
**Agent Decision:** May use Claude Code tool for implementation → Provides both explanation and code files
## Git Integration
When Claude Code makes changes and git is available:
1. **Automatic Branch Creation:** Creates `claude-code-YYYYMMDD_HHMMSS` branch
2. **Commit Changes:** Commits all modifications with descriptive message
3. **Remote Push:** Pushes branch to remote repository (if configured)
4. **PR URL Generation:** Generates GitHub PR creation URL
Example output:
```
📋 **Pull Request Created:**
https://github.com/user/repo/compare/main...claude-code-20241210_143022?quick_pull=1
```
## Error Handling
### Graceful Degradation
- If `praisonaiagents` unavailable → Falls back to `litellm`
- If Claude Code CLI unavailable → Agent works without the tool
- If git unavailable → Code changes without git operations
- Network issues → Continues with local operations
### Error Messages
- Clear error reporting in streaming responses
- Timeout handling for long operations
- Detailed logging for debugging
## Implementation Details
### File Structure
```
src/praisonai/praisonai/
├── cli.py # Added --claudecode flag
└── ui/
└── code.py # Refactored to use praisonaiagents
```
### Key Functions
1. **`handle_with_praisonai_agents()`** - Main agent execution handler
2. **`handle_with_litellm()`** - Fallback for backward compatibility
3. **`claude_code_tool()`** - Claude Code tool implementation
4. **Settings management** - UI toggles and persistence
### Dependencies
**Required:**
- `praisonaiagents` - Core agent framework
- `subprocess` - For Claude Code CLI execution
- `chainlit` - UI framework
**Optional:**
- `tavily` - Web search capability
- `git` - Version control operations
- `claude` CLI - File modification capabilities
## Testing
Use the included test script to verify the integration:
```bash
python test_claude_code_integration.py
```
Tests verify:
- Import functionality
- PraisonAI Agents availability
- Claude Code CLI availability
- Environment variable configuration
- Basic tool execution
## Migration Guide
### From Previous Implementation
The previous implementation used direct Claude Code detection logic. The new implementation:
1. **Replaces detection logic** with agent intelligence
2. **Maintains same UI/CLI interfaces** for easy migration
3. **Preserves all existing functionality** while adding flexibility
4. **Requires no user workflow changes**
### Upgrading
1. Ensure `praisonaiagents` is installed
2. Update PraisonAI to include the refactored code
3. Use `--claudecode` flag or UI toggle as before
4. Existing settings and workflows continue working
## Best Practices
### When to Enable Claude Code
- File modification projects
- Code development workflows
- Repository maintenance tasks
- Implementation and debugging sessions
### When to Disable Claude Code
- Read-only analysis
- Documentation-only sessions
- Learning and educational content
- General Q&A conversations
### Configuration Recommendations
- Use CLI flag for development sessions
- Use UI toggle for mixed workflows
- Set environment variables for CI/CD integration
- Configure repository path for multi-project setups
## Troubleshooting
### Common Issues
**Agent doesn't use Claude Code tool:**
- Verify Claude Code is enabled (`--claudecode` flag or UI toggle)
- Check that request involves file modifications
- Ensure `praisonaiagents` is installed
**Claude Code tool fails:**
- Verify `claude` CLI is installed and in PATH
- Check repository permissions
- Confirm git repository status (if git operations needed)
**Fallback to litellm:**
- Install `praisonaiagents`: `pip install praisonaiagents`
- Check import errors in logs
- Verify Python environment compatibility
### Debug Information
Enable verbose logging:
```bash
export LOGLEVEL=DEBUG
praisonai code --claudecode
```
Check environment:
```python
import os
print("Claude Code enabled:", os.getenv("PRAISONAI_CLAUDECODE_ENABLED"))
print("Repo path:", os.getenv("PRAISONAI_CODE_REPO_PATH"))
```
## Future Enhancements
### Planned Features
- Real-time streaming from PraisonAI Agents
- Enhanced git workflow integration
- Multi-repository support
- Advanced tool coordination
- Performance optimizations
### Extension Points
- Additional coding tools integration
- Custom agent instructions
- Workflow templates
- Integration with other development tools
---
For technical support or questions, please refer to the main PraisonAI documentation or submit issues to the repository.
Whitespace-only changes.
@@ -0,0 +1,477 @@
"""
AG2 Mock Integration Tests — verifies PraisonAI correctly orchestrates AG2 agents.
Uses mocked AG2 responses — no real LLM API calls are made.
Fast: all tests should complete in < 1s each.
Run:
pytest tests/integration/ag2/test_ag2_integration.py -v
"""
import pytest
import os
import sys
from unittest.mock import patch, MagicMock
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../../"))
# Stub heavy dependencies that auto.py (develop branch) imports at module level
# so that tests can import praisonai without a full installation.
for _stub in ("instructor",):
if _stub not in sys.modules:
sys.modules[_stub] = MagicMock()
import importlib as _importlib
if "openai" not in sys.modules:
try:
_importlib.import_module("openai")
except ImportError:
_mock_openai = MagicMock()
_mock_openai.__version__ = "1.0.0"
sys.modules["openai"] = _mock_openai
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def single_agent_yaml():
return """
framework: ag2
topic: Write a short poem about Python
roles:
poet:
role: "Creative Poet"
goal: "Write elegant and concise poems"
backstory: "You are a creative poet who loves Python and technology."
tasks:
write_poem:
description: "Write a short 4-line poem about Python programming"
expected_output: "A 4-line poem about Python"
"""
@pytest.fixture
def multi_agent_yaml():
return """
framework: ag2
topic: Explain why open-source AI matters
roles:
researcher:
role: "Research Analyst"
goal: "Research and gather key facts"
backstory: "Expert in technology research and trend analysis."
tasks:
research:
description: "Research why open-source AI matters"
expected_output: "Key facts and data points"
writer:
role: "Technical Writer"
goal: "Write clear technical content"
backstory: "Professional technical writer with 10 years experience."
tasks:
write:
description: "Write a clear explanation of why open-source AI matters"
expected_output: "A 200-word explanation"
"""
@pytest.fixture
def mock_ag2_classes():
"""Mock all AG2 classes used in _run_ag2."""
mock_llm_config = MagicMock()
mock_llm_config.__enter__ = MagicMock(return_value=mock_llm_config)
mock_llm_config.__exit__ = MagicMock(return_value=False)
mock_assistant = MagicMock()
mock_assistant.name = "test_agent"
mock_user_proxy = MagicMock()
mock_user_proxy.name = "User"
mock_groupchat = MagicMock()
mock_groupchat.messages = [
{
"name": "test_agent",
"role": "assistant",
"content": "Task completed successfully. TERMINATE",
}
]
mock_manager = MagicMock()
return {
"llm_config": mock_llm_config,
"assistant": mock_assistant,
"user_proxy": mock_user_proxy,
"groupchat": mock_groupchat,
"manager": mock_manager,
}
# ---------------------------------------------------------------------------
# AG2 import detection
# ---------------------------------------------------------------------------
class TestAG2Import:
@pytest.mark.integration
def test_ag2_distribution_available(self):
"""ag2 PyPI distribution should be findable when installed.
Note: ag2 installs under the 'autogen' namespace — 'import ag2' does NOT work.
Detection uses importlib.metadata to check the 'ag2' distribution name.
"""
import importlib.metadata
try:
dist = importlib.metadata.distribution('ag2')
assert dist.metadata['Name'] == 'ag2'
except importlib.metadata.PackageNotFoundError:
pytest.skip("ag2 not installed — skipping AG2 integration tests")
@pytest.mark.integration
def test_autogen_namespace_importable_with_ag2(self):
"""When ag2 is installed, autogen namespace classes are importable."""
try:
from autogen import AssistantAgent, UserProxyAgent, GroupChat, LLMConfig
assert AssistantAgent is not None
assert UserProxyAgent is not None
assert GroupChat is not None
assert LLMConfig is not None
except ImportError:
pytest.skip("ag2 not installed — autogen namespace not available")
# ---------------------------------------------------------------------------
# Single-agent flow (mocked)
# ---------------------------------------------------------------------------
class TestAG2SingleAgentFlow:
@pytest.mark.integration
def test_single_agent_yaml_initialises_praisonai(self, single_agent_yaml, mock_ag2_classes):
"""PraisonAI initialises correctly with framework='ag2' from YAML."""
import tempfile
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
f.write(single_agent_yaml)
yaml_path = f.name
try:
with patch("praisonai.agents_generator.AG2_AVAILABLE", True), \
patch("praisonai.agents_generator.PRAISONAI_AVAILABLE", True):
try:
from praisonai import PraisonAI
ai = PraisonAI(agent_file=yaml_path, framework="ag2")
assert ai.framework == "ag2"
except ImportError as e:
pytest.skip(f"PraisonAI not available: {e}")
finally:
os.unlink(yaml_path)
@pytest.mark.integration
def test_single_agent_run_returns_ag2_output(self, mock_ag2_classes):
"""_run_ag2 executes single-agent flow and returns '### AG2 Output ###'."""
config = {
"framework": "ag2",
"topic": "Write a poem",
"roles": {
"poet": {
"role": "Creative Poet",
"goal": "Write poems",
"backstory": "A creative poet.",
"tasks": {
"task1": {
"description": "Write a poem about Python",
"expected_output": "A short poem",
}
},
"tools": [],
}
},
}
m = mock_ag2_classes
with patch("praisonai.agents_generator.AG2_AVAILABLE", True), \
patch("praisonai.agents_generator.CREWAI_AVAILABLE", False), \
patch("praisonai.agents_generator.AUTOGEN_AVAILABLE", False), \
patch("praisonai.agents_generator.PRAISONAI_AVAILABLE", True):
try:
from praisonai.agents_generator import AgentsGenerator
except ImportError as e:
pytest.skip(f"AgentsGenerator not available: {e}")
gen = AgentsGenerator(
agent_file="agents.yaml",
framework="ag2",
config_list=[{"model": "gpt-4o-mini", "api_key": "sk-test"}],
)
with patch("autogen.LLMConfig", create=True, return_value=m["llm_config"]), \
patch("autogen.AssistantAgent", create=True, return_value=m["assistant"]), \
patch("autogen.UserProxyAgent", create=True, return_value=m["user_proxy"]), \
patch("autogen.GroupChat", create=True, return_value=m["groupchat"]), \
patch("autogen.GroupChatManager", create=True, return_value=m["manager"]):
result = gen._run_ag2(config, "Write a poem", {})
assert "### AG2 Output ###" in result
assert "Task completed successfully" in result
@pytest.mark.integration
def test_single_agent_calls_initiate_chat(self, mock_ag2_classes):
"""user_proxy.initiate_chat is called with the manager and initial message."""
config = {
"framework": "ag2",
"topic": "Test topic",
"roles": {
"agent": {
"role": "Agent",
"goal": "Help",
"backstory": "Helpful agent.",
"tasks": {
"t": {"description": "Do the task", "expected_output": "Done"}
},
"tools": [],
}
},
}
m = mock_ag2_classes
with patch("praisonai.agents_generator.AG2_AVAILABLE", True), \
patch("praisonai.agents_generator.CREWAI_AVAILABLE", False), \
patch("praisonai.agents_generator.AUTOGEN_AVAILABLE", False), \
patch("praisonai.agents_generator.PRAISONAI_AVAILABLE", True):
try:
from praisonai.agents_generator import AgentsGenerator
except ImportError as e:
pytest.skip(f"AgentsGenerator not available: {e}")
gen = AgentsGenerator(
agent_file="agents.yaml",
framework="ag2",
config_list=[{"model": "gpt-4o-mini", "api_key": "sk-test"}],
)
with patch("autogen.LLMConfig", create=True, return_value=m["llm_config"]), \
patch("autogen.AssistantAgent", create=True, return_value=m["assistant"]), \
patch("autogen.UserProxyAgent", create=True, return_value=m["user_proxy"]), \
patch("autogen.GroupChat", create=True, return_value=m["groupchat"]), \
patch("autogen.GroupChatManager", create=True, return_value=m["manager"]):
gen._run_ag2(config, "Test topic", {})
m["user_proxy"].initiate_chat.assert_called_once()
call_args = m["user_proxy"].initiate_chat.call_args
# First positional arg should be the manager
assert call_args[0][0] is m["manager"]
# message kwarg should be non-empty (task description or topic)
message = call_args[1].get("message", "")
assert message != ""
# ---------------------------------------------------------------------------
# Multi-agent GroupChat flow (mocked)
# ---------------------------------------------------------------------------
class TestAG2MultiAgentGroupChatFlow:
@pytest.mark.integration
def test_multi_agent_creates_correct_number_of_assistants(self, multi_agent_yaml, mock_ag2_classes):
"""Two roles in YAML → two AssistantAgents created."""
config = {
"framework": "ag2",
"topic": "Explain open-source AI",
"roles": {
"researcher": {
"role": "Research Analyst", "goal": "Research", "backstory": "Analyst.",
"tasks": {"r": {"description": "Research it", "expected_output": "Facts"}},
"tools": [],
},
"writer": {
"role": "Technical Writer", "goal": "Write", "backstory": "Writer.",
"tasks": {"w": {"description": "Write it", "expected_output": "Article"}},
"tools": [],
},
},
}
m = mock_ag2_classes
assistant_call_count = [0]
def count_assistant(**kwargs):
assistant_call_count[0] += 1
a = MagicMock()
a.name = kwargs.get("name", f"agent_{assistant_call_count[0]}")
return a
with patch("praisonai.agents_generator.AG2_AVAILABLE", True), \
patch("praisonai.agents_generator.CREWAI_AVAILABLE", False), \
patch("praisonai.agents_generator.AUTOGEN_AVAILABLE", False), \
patch("praisonai.agents_generator.PRAISONAI_AVAILABLE", True):
try:
from praisonai.agents_generator import AgentsGenerator
except ImportError as e:
pytest.skip(f"AgentsGenerator not available: {e}")
gen = AgentsGenerator(
agent_file="agents.yaml",
framework="ag2",
config_list=[{"model": "gpt-4o-mini", "api_key": "sk-test"}],
)
with patch("autogen.LLMConfig", create=True, return_value=m["llm_config"]), \
patch("autogen.AssistantAgent", create=True, side_effect=count_assistant), \
patch("autogen.UserProxyAgent", create=True, return_value=m["user_proxy"]), \
patch("autogen.GroupChat", create=True, return_value=m["groupchat"]), \
patch("autogen.GroupChatManager", create=True, return_value=m["manager"]):
gen._run_ag2(config, "Explain open-source AI", {})
assert assistant_call_count[0] == 2
@pytest.mark.integration
def test_multi_agent_groupchat_max_round_set(self, mock_ag2_classes):
"""GroupChat is created with max_round parameter."""
config = {
"framework": "ag2",
"topic": "Test",
"roles": {
"a": {
"role": "Agent", "goal": "Help", "backstory": "Helper.",
"tasks": {"t": {"description": "Do", "expected_output": "Done"}},
"tools": [],
}
},
}
m = mock_ag2_classes
groupchat_kwargs = {}
def capture_groupchat(**kwargs):
groupchat_kwargs.update(kwargs)
return m["groupchat"]
with patch("praisonai.agents_generator.AG2_AVAILABLE", True), \
patch("praisonai.agents_generator.CREWAI_AVAILABLE", False), \
patch("praisonai.agents_generator.AUTOGEN_AVAILABLE", False), \
patch("praisonai.agents_generator.PRAISONAI_AVAILABLE", True):
try:
from praisonai.agents_generator import AgentsGenerator
except ImportError as e:
pytest.skip(f"AgentsGenerator not available: {e}")
gen = AgentsGenerator(
agent_file="agents.yaml",
framework="ag2",
config_list=[{"model": "gpt-4o-mini", "api_key": "sk-test"}],
)
with patch("autogen.LLMConfig", create=True, return_value=m["llm_config"]), \
patch("autogen.AssistantAgent", create=True, return_value=m["assistant"]), \
patch("autogen.UserProxyAgent", create=True, return_value=m["user_proxy"]), \
patch("autogen.GroupChat", create=True, side_effect=capture_groupchat), \
patch("autogen.GroupChatManager", create=True, return_value=m["manager"]):
gen._run_ag2(config, "Test", {})
assert "max_round" in groupchat_kwargs
assert groupchat_kwargs["max_round"] > 0
# ---------------------------------------------------------------------------
# Backward-compatibility: existing autogen/crewai paths unaffected
# ---------------------------------------------------------------------------
class TestAG2BackwardCompatibility:
@pytest.mark.integration
def test_autogen_framework_still_works(self):
"""framework='autogen' dispatches to _run_autogen, not _run_ag2."""
import yaml
with patch("praisonai.agents_generator.AUTOGEN_AVAILABLE", True), \
patch("praisonai.agents_generator.AG2_AVAILABLE", False), \
patch("praisonai.agents_generator.CREWAI_AVAILABLE", False), \
patch("praisonai.agents_generator.PRAISONAI_AVAILABLE", True):
try:
from praisonai.agents_generator import AgentsGenerator
except ImportError as e:
pytest.skip(f"AgentsGenerator not available: {e}")
gen = AgentsGenerator(
agent_file="agents.yaml",
framework="autogen",
config_list=[{"model": "gpt-4o-mini", "api_key": "sk-test"}],
)
assert gen.framework == "autogen"
gen.agent_yaml = yaml.dump({
"framework": "autogen",
"topic": "Test",
"roles": {
"agent": {
"role": "Agent", "goal": "Help", "backstory": "Helper.",
"tasks": {"t": {"description": "Do", "expected_output": "Done"}},
"tools": [],
}
},
})
# Verify _run_ag2 is NOT called when framework='autogen'
with patch.object(gen, "_run_ag2") as mock_run_ag2, \
patch.object(gen, "_run_autogen", return_value="autogen result") as mock_run_autogen:
gen.generate_crew_and_kickoff()
mock_run_ag2.assert_not_called()
mock_run_autogen.assert_called_once()
@pytest.mark.integration
def test_ag2_framework_dispatches_to_run_ag2(self, mock_ag2_classes):
"""framework='ag2' dispatches to _run_ag2 and NOT to _run_autogen."""
import yaml
m = mock_ag2_classes
with patch("praisonai.agents_generator.AG2_AVAILABLE", True), \
patch("praisonai.agents_generator.CREWAI_AVAILABLE", False), \
patch("praisonai.agents_generator.AUTOGEN_AVAILABLE", False), \
patch("praisonai.agents_generator.PRAISONAI_AVAILABLE", True):
try:
from praisonai.agents_generator import AgentsGenerator
except ImportError as e:
pytest.skip(f"AgentsGenerator not available: {e}")
gen = AgentsGenerator(
agent_file="agents.yaml",
framework="ag2",
config_list=[{"model": "gpt-4o-mini", "api_key": "sk-test"}],
)
gen.agent_yaml = yaml.dump({
"framework": "ag2",
"topic": "Test",
"roles": {
"agent": {
"role": "Agent", "goal": "Help", "backstory": "Helper.",
"tasks": {"t": {"description": "Do", "expected_output": "Done"}},
"tools": [],
}
},
})
with patch.object(gen, "_run_autogen") as mock_autogen, \
patch("autogen.LLMConfig", create=True, return_value=m["llm_config"]), \
patch("autogen.AssistantAgent", create=True, return_value=m["assistant"]), \
patch("autogen.UserProxyAgent", create=True, return_value=m["user_proxy"]), \
patch("autogen.GroupChat", create=True, return_value=m["groupchat"]), \
patch("autogen.GroupChatManager", create=True, return_value=m["manager"]):
result = gen.generate_crew_and_kickoff()
mock_autogen.assert_not_called()
assert "### AG2 Output ###" in result
if __name__ == "__main__":
pytest.main([__file__, "-v"])
@@ -0,0 +1,98 @@
"""
Example of AG2 tool registration for use with PraisonAI's --framework ag2.
AG2 (community fork of AutoGen, PyPI package: ag2) installs under the
'autogen' namespace but is a distinct package from pyautogen/pyautogen2.
Usage with PraisonAI:
pip install "praisonai[ag2]"
praisonai --framework ag2 agents.yaml
Standalone run:
python tests/source/ag2_function_tools.py
"""
from typing import Annotated, Literal
import os
# ag2 installs under the 'autogen' namespace
from autogen import (
AssistantAgent,
UserProxyAgent,
GroupChat,
GroupChatManager,
LLMConfig,
)
Operator = Literal["+", "-", "*", "/"]
def calculator(
a: Annotated[int, "First operand"],
b: Annotated[int, "Second operand"],
operator: Annotated[Operator, "Arithmetic operator: +, -, *, /"],
) -> int:
"""Perform basic arithmetic operations."""
if operator == "+":
return a + b
elif operator == "-":
return a - b
elif operator == "*":
return a * b
elif operator == "/":
if b == 0:
raise ValueError("Division by zero")
return int(a / b)
else:
raise ValueError(f"Invalid operator: {operator}")
# Build LLMConfig — AG2 uses a context manager pattern so agents
# created inside the 'with' block automatically inherit the config.
llm_config = LLMConfig(
api_type="openai",
model=os.environ.get("MODEL_NAME", "gpt-4o-mini"),
api_key=os.environ.get("OPENAI_API_KEY"),
)
# Create AssistantAgent inside llm_config context
with llm_config:
assistant = AssistantAgent(
name="Calculator_Assistant",
system_message=(
"You are a helpful AI assistant that can perform arithmetic calculations. "
"Use the calculator tool when math operations are needed. "
"Return 'TERMINATE' when the task is done."
),
)
# UserProxyAgent does not use LLM — created outside context
user_proxy = UserProxyAgent(
name="User",
human_input_mode="NEVER",
is_termination_msg=lambda msg: (
msg.get("content") is not None and "TERMINATE" in msg["content"]
),
code_execution_config=False,
)
# AG2 tool registration pattern:
# @agent.register_for_llm(description="...") — exposes tool schema to the LLM
# @user_proxy.register_for_execution() — tells user_proxy to execute it
@assistant.register_for_llm(description="A simple calculator for arithmetic operations.")
@user_proxy.register_for_execution()
def calculator_tool(
a: Annotated[int, "First operand"],
b: Annotated[int, "Second operand"],
operator: Annotated[Operator, "Arithmetic operator"],
) -> int:
return calculator(a, b, operator)
if __name__ == "__main__":
chat_result = user_proxy.initiate_chat(
assistant,
message="What is (44232 + 13312 / (232 - 32)) * 5?",
)
print(f"\nResult: {chat_result.summary}")
@@ -0,0 +1,542 @@
"""
Unit tests for the AG2 framework integration in PraisonAI.
Tests cover:
- AG2 availability detection (AG2_AVAILABLE flag)
- Framework validation in AgentsGenerator.__init__
- _run_ag2 method: config parsing, agent creation, tool handling
- LLMConfig construction (OpenAI and Bedrock paths)
All external calls are mocked — no real LLM API calls made.
"""
import pytest
import os
import sys
from unittest.mock import patch, MagicMock, call
import importlib
# Ensure src is on path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../"))
# Stub heavy dependencies that auto.py (develop branch) imports at module level
# so that tests can import praisonai without a full installation.
for _stub in ("instructor",):
if _stub not in sys.modules:
sys.modules[_stub] = MagicMock()
# openai is installed (required by ag2/autogen internals), but auto.py also
# imports it at module level. Ensure it's really loaded, not a mock.
import importlib as _importlib
if "openai" not in sys.modules:
try:
_importlib.import_module("openai")
except ImportError:
_mock_openai = MagicMock()
_mock_openai.__version__ = "1.0.0"
sys.modules["openai"] = _mock_openai
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_config(roles=None, framework="ag2", topic="Test topic"):
"""Build a minimal agents config dict."""
if roles is None:
roles = {
"researcher": {
"role": "Researcher",
"goal": "Research things",
"backstory": "You are a researcher.",
"tasks": {
"task1": {
"description": "Research {topic}",
"expected_output": "A research report",
}
},
"tools": [],
}
}
return {"framework": framework, "topic": topic, "roles": roles}
# ---------------------------------------------------------------------------
# AG2_AVAILABLE flag detection
# ---------------------------------------------------------------------------
class TestAG2AvailabilityFlag:
def test_ag2_available_false_when_not_installed(self):
"""AG2_AVAILABLE is False when 'ag2' distribution is not found."""
import importlib.metadata as meta
original_fn = meta.distribution
def raise_not_found(name):
if name == 'ag2':
raise meta.PackageNotFoundError('ag2')
return original_fn(name)
# Verify detection logic directly without relying on cached module state
ag2_detected = True
try:
with patch('importlib.metadata.distribution', side_effect=raise_not_found):
meta.distribution('ag2')
except meta.PackageNotFoundError:
ag2_detected = False
assert ag2_detected is False
def test_ag2_available_true_when_installed(self):
"""AG2_AVAILABLE is True when 'ag2' distribution and LLMConfig are present."""
import importlib.metadata as meta
try:
dist = meta.distribution('ag2')
assert dist is not None
from autogen import LLMConfig # noqa: F401
except (meta.PackageNotFoundError, ImportError):
pytest.skip("ag2 not installed — skipping")
# ---------------------------------------------------------------------------
# AgentsGenerator.__init__ framework validation
# ---------------------------------------------------------------------------
class TestAgentsGeneratorAG2Validation:
def _make_generator(self, framework, ag2_available=True):
"""Create AgentsGenerator with mocked availability flags."""
with patch("praisonai.agents_generator.AG2_AVAILABLE", ag2_available), \
patch("praisonai.agents_generator.CREWAI_AVAILABLE", False), \
patch("praisonai.agents_generator.AUTOGEN_AVAILABLE", False), \
patch("praisonai.agents_generator.PRAISONAI_AVAILABLE", True):
from praisonai.agents_generator import AgentsGenerator
return AgentsGenerator(
agent_file="agents.yaml",
framework=framework,
config_list=[{"model": "gpt-4o-mini", "api_key": "test-key"}],
)
def test_ag2_framework_accepted_when_available(self):
"""No ImportError when framework='ag2' and AG2 is installed."""
gen = self._make_generator("ag2", ag2_available=True)
assert gen.framework == "ag2"
def test_ag2_framework_raises_when_not_available(self):
"""ImportError raised with helpful message when ag2 not installed."""
with pytest.raises(ImportError, match="AG2 is not installed"):
self._make_generator("ag2", ag2_available=False)
def test_autogen_framework_unaffected(self):
"""Existing autogen framework path still works independently."""
with patch("praisonai.agents_generator.AG2_AVAILABLE", False), \
patch("praisonai.agents_generator.AUTOGEN_AVAILABLE", True), \
patch("praisonai.agents_generator.CREWAI_AVAILABLE", False), \
patch("praisonai.agents_generator.PRAISONAI_AVAILABLE", True):
from praisonai.agents_generator import AgentsGenerator
gen = AgentsGenerator(
agent_file="agents.yaml",
framework="autogen",
config_list=[{"model": "gpt-4o-mini", "api_key": "test-key"}],
)
assert gen.framework == "autogen"
# ---------------------------------------------------------------------------
# _run_ag2: LLMConfig construction
# ---------------------------------------------------------------------------
class TestRunAG2LLMConfig:
def _make_gen_with_config(self, config_list):
"""Build an AgentsGenerator instance with given config_list."""
with patch("praisonai.agents_generator.AG2_AVAILABLE", True), \
patch("praisonai.agents_generator.CREWAI_AVAILABLE", False), \
patch("praisonai.agents_generator.AUTOGEN_AVAILABLE", False), \
patch("praisonai.agents_generator.PRAISONAI_AVAILABLE", True):
from praisonai.agents_generator import AgentsGenerator
return AgentsGenerator(
agent_file="agents.yaml",
framework="ag2",
config_list=config_list,
)
@patch("praisonai.agents_generator.AG2_AVAILABLE", True)
def test_openai_llm_config_constructed(self):
"""LLMConfig is built with api_type='openai' for standard config."""
gen = self._make_gen_with_config([
{"model": "gpt-4o-mini", "api_key": "sk-test", "api_type": "openai"}
])
mock_llm_config = MagicMock()
mock_llm_config.__enter__ = MagicMock(return_value=mock_llm_config)
mock_llm_config.__exit__ = MagicMock(return_value=False)
mock_assistant = MagicMock()
mock_user_proxy = MagicMock()
mock_groupchat = MagicMock()
mock_groupchat.messages = [{"name": "Researcher", "content": "Done. TERMINATE", "role": "assistant"}]
mock_manager = MagicMock()
with patch("praisonai.agents_generator.AG2_AVAILABLE", True), \
patch("autogen.LLMConfig", create=True, return_value=mock_llm_config) as mock_llmcfg, \
patch("autogen.AssistantAgent", create=True, return_value=mock_assistant), \
patch("autogen.UserProxyAgent", create=True, return_value=mock_user_proxy), \
patch("autogen.GroupChat", create=True, return_value=mock_groupchat), \
patch("autogen.GroupChatManager", create=True, return_value=mock_manager):
config = _make_config()
gen._run_ag2(config, "Test topic", {})
mock_llmcfg.assert_called_once()
# LLMConfig is called with a positional dict arg
call_args = mock_llmcfg.call_args[0][0]
assert call_args.get("model") == "gpt-4o-mini"
assert call_args.get("api_key") == "sk-test"
@patch("praisonai.agents_generator.AG2_AVAILABLE", True)
def test_bedrock_llm_config_constructed(self):
"""LLMConfig uses api_type='bedrock' when config specifies bedrock."""
gen = self._make_gen_with_config([
{
"model": "anthropic.claude-3-5-sonnet-20241022-v2:0",
"api_type": "bedrock",
}
])
mock_llm_config = MagicMock()
mock_llm_config.__enter__ = MagicMock(return_value=mock_llm_config)
mock_llm_config.__exit__ = MagicMock(return_value=False)
mock_assistant = MagicMock()
mock_user_proxy = MagicMock()
mock_groupchat = MagicMock()
mock_groupchat.messages = [{"name": "Agent", "content": "Report ready. TERMINATE", "role": "assistant"}]
mock_manager = MagicMock()
with patch("autogen.LLMConfig", create=True, return_value=mock_llm_config) as mock_llmcfg, \
patch("autogen.AssistantAgent", create=True, return_value=mock_assistant), \
patch("autogen.UserProxyAgent", create=True, return_value=mock_user_proxy), \
patch("autogen.GroupChat", create=True, return_value=mock_groupchat), \
patch("autogen.GroupChatManager", create=True, return_value=mock_manager):
config = _make_config()
gen._run_ag2(config, "AWS deployment", {})
mock_llmcfg.assert_called_once()
# LLMConfig is called with a positional dict arg
call_args = mock_llmcfg.call_args[0][0]
assert call_args.get("api_type") == "bedrock"
assert "api_key" not in call_args # no api_key for bedrock
# ---------------------------------------------------------------------------
# _run_ag2: agent and GroupChat creation
# ---------------------------------------------------------------------------
class TestRunAG2AgentCreation:
def _make_gen(self):
with patch("praisonai.agents_generator.AG2_AVAILABLE", True), \
patch("praisonai.agents_generator.CREWAI_AVAILABLE", False), \
patch("praisonai.agents_generator.AUTOGEN_AVAILABLE", False), \
patch("praisonai.agents_generator.PRAISONAI_AVAILABLE", True):
from praisonai.agents_generator import AgentsGenerator
return AgentsGenerator(
agent_file="agents.yaml",
framework="ag2",
config_list=[{"model": "gpt-4o-mini", "api_key": "sk-test"}],
)
def test_assistant_created_per_role(self):
"""One AssistantAgent is created for each role in the config."""
gen = self._make_gen()
config = _make_config(roles={
"role_a": {
"role": "Agent A", "goal": "Goal A", "backstory": "Backstory A",
"tasks": {"t1": {"description": "Do A", "expected_output": "A done"}},
"tools": [],
},
"role_b": {
"role": "Agent B", "goal": "Goal B", "backstory": "Backstory B",
"tasks": {"t2": {"description": "Do B", "expected_output": "B done"}},
"tools": [],
},
})
mock_llm_config = MagicMock()
mock_llm_config.__enter__ = MagicMock(return_value=mock_llm_config)
mock_llm_config.__exit__ = MagicMock(return_value=False)
created_agents = []
def fake_assistant(**kwargs):
m = MagicMock()
m.name = kwargs.get("name", "agent")
created_agents.append(m)
return m
mock_user_proxy = MagicMock()
mock_groupchat = MagicMock()
mock_groupchat.messages = [{"name": "Agent A", "content": "Done. TERMINATE", "role": "assistant"}]
mock_manager = MagicMock()
with patch("autogen.LLMConfig", create=True, return_value=mock_llm_config), \
patch("autogen.AssistantAgent", create=True, side_effect=fake_assistant), \
patch("autogen.UserProxyAgent", create=True, return_value=mock_user_proxy), \
patch("autogen.GroupChat", create=True, return_value=mock_groupchat), \
patch("autogen.GroupChatManager", create=True, return_value=mock_manager):
gen._run_ag2(config, "Test", {})
assert len(created_agents) == 2
def test_groupchat_includes_user_proxy_and_assistants(self):
"""GroupChat receives user_proxy + all assistants."""
gen = self._make_gen()
config = _make_config()
mock_llm_config = MagicMock()
mock_llm_config.__enter__ = MagicMock(return_value=mock_llm_config)
mock_llm_config.__exit__ = MagicMock(return_value=False)
mock_assistant = MagicMock()
mock_assistant.name = "Researcher"
mock_user_proxy = MagicMock()
mock_user_proxy.name = "User"
mock_groupchat = MagicMock()
mock_groupchat.messages = [{"name": "Researcher", "content": "Done. TERMINATE", "role": "assistant"}]
mock_manager = MagicMock()
groupchat_call_args = {}
def capture_groupchat(**kwargs):
groupchat_call_args.update(kwargs)
return mock_groupchat
with patch("autogen.LLMConfig", create=True, return_value=mock_llm_config), \
patch("autogen.AssistantAgent", create=True, return_value=mock_assistant), \
patch("autogen.UserProxyAgent", create=True, return_value=mock_user_proxy), \
patch("autogen.GroupChat", create=True, side_effect=capture_groupchat), \
patch("autogen.GroupChatManager", create=True, return_value=mock_manager):
gen._run_ag2(config, "Test", {})
agents_in_groupchat = groupchat_call_args.get("agents", [])
assert mock_user_proxy in agents_in_groupchat
assert mock_assistant in agents_in_groupchat
def test_empty_roles_returns_no_agents_message(self):
"""Returns a clear message when no roles are defined in config."""
gen = self._make_gen()
config = _make_config(roles={})
mock_llm_config = MagicMock()
mock_llm_config.__enter__ = MagicMock(return_value=mock_llm_config)
mock_llm_config.__exit__ = MagicMock(return_value=False)
mock_user_proxy = MagicMock()
with patch("autogen.LLMConfig", create=True, return_value=mock_llm_config), \
patch("autogen.AssistantAgent"), \
patch("autogen.UserProxyAgent", create=True, return_value=mock_user_proxy):
result = gen._run_ag2(config, "Test", {})
assert "No agents" in result
# ---------------------------------------------------------------------------
# _run_ag2: system message composition
# ---------------------------------------------------------------------------
class TestRunAG2SystemMessage:
def _make_gen(self):
with patch("praisonai.agents_generator.AG2_AVAILABLE", True), \
patch("praisonai.agents_generator.CREWAI_AVAILABLE", False), \
patch("praisonai.agents_generator.AUTOGEN_AVAILABLE", False), \
patch("praisonai.agents_generator.PRAISONAI_AVAILABLE", True):
from praisonai.agents_generator import AgentsGenerator
return AgentsGenerator(
agent_file="agents.yaml",
framework="ag2",
config_list=[{"model": "gpt-4o-mini", "api_key": "sk-test"}],
)
def test_system_message_contains_backstory(self):
"""AssistantAgent system_message includes the backstory from YAML."""
gen = self._make_gen()
config = _make_config(roles={
"agent1": {
"role": "Expert",
"goal": "Help users",
"backstory": "A unique backstory for testing purposes.",
"tasks": {"t": {"description": "Do it", "expected_output": "Done"}},
"tools": [],
}
})
mock_llm_config = MagicMock()
mock_llm_config.__enter__ = MagicMock(return_value=mock_llm_config)
mock_llm_config.__exit__ = MagicMock(return_value=False)
mock_user_proxy = MagicMock()
mock_groupchat = MagicMock()
mock_groupchat.messages = [{"name": "Expert", "content": "Done. TERMINATE", "role": "assistant"}]
mock_manager = MagicMock()
assistant_kwargs = {}
def capture_assistant(**kwargs):
assistant_kwargs.update(kwargs)
m = MagicMock()
m.name = kwargs.get("name", "agent")
return m
with patch("autogen.LLMConfig", create=True, return_value=mock_llm_config), \
patch("autogen.AssistantAgent", create=True, side_effect=capture_assistant), \
patch("autogen.UserProxyAgent", create=True, return_value=mock_user_proxy), \
patch("autogen.GroupChat", create=True, return_value=mock_groupchat), \
patch("autogen.GroupChatManager", create=True, return_value=mock_manager):
gen._run_ag2(config, "Test", {})
assert "A unique backstory for testing purposes." in assistant_kwargs.get("system_message", "")
assert "TERMINATE" in assistant_kwargs.get("system_message", "")
def test_agent_name_sanitised(self):
"""Agent names with special characters are sanitised for AG2 compatibility."""
gen = self._make_gen()
config = _make_config(roles={
"agent1": {
"role": "AI & ML Expert (2024)", # contains special chars
"goal": "Help",
"backstory": "Expert.",
"tasks": {"t": {"description": "Do it", "expected_output": "Done"}},
"tools": [],
}
})
mock_llm_config = MagicMock()
mock_llm_config.__enter__ = MagicMock(return_value=mock_llm_config)
mock_llm_config.__exit__ = MagicMock(return_value=False)
mock_user_proxy = MagicMock()
mock_groupchat = MagicMock()
mock_groupchat.messages = [{"name": "AI___ML_Expert__2024_", "content": "Done. TERMINATE", "role": "assistant"}]
mock_manager = MagicMock()
created_name = {}
def capture_assistant(**kwargs):
created_name["name"] = kwargs.get("name", "")
m = MagicMock()
m.name = created_name["name"]
return m
with patch("autogen.LLMConfig", create=True, return_value=mock_llm_config), \
patch("autogen.AssistantAgent", create=True, side_effect=capture_assistant), \
patch("autogen.UserProxyAgent", create=True, return_value=mock_user_proxy), \
patch("autogen.GroupChat", create=True, return_value=mock_groupchat), \
patch("autogen.GroupChatManager", create=True, return_value=mock_manager):
gen._run_ag2(config, "Test", {})
name = created_name.get("name", "")
# Should not contain special chars that would break AG2
import re
assert re.match(r"^[a-zA-Z0-9_\-]+$", name), f"Name '{name}' contains invalid characters"
# ---------------------------------------------------------------------------
# _run_ag2: output extraction
# ---------------------------------------------------------------------------
class TestRunAG2OutputExtraction:
def _make_gen(self):
with patch("praisonai.agents_generator.AG2_AVAILABLE", True), \
patch("praisonai.agents_generator.CREWAI_AVAILABLE", False), \
patch("praisonai.agents_generator.AUTOGEN_AVAILABLE", False), \
patch("praisonai.agents_generator.PRAISONAI_AVAILABLE", True):
from praisonai.agents_generator import AgentsGenerator
return AgentsGenerator(
agent_file="agents.yaml",
framework="ag2",
config_list=[{"model": "gpt-4o-mini", "api_key": "sk-test"}],
)
def _run_with_messages(self, messages):
gen = self._make_gen()
config = _make_config()
mock_llm_config = MagicMock()
mock_assistant = MagicMock()
mock_assistant.name = "Researcher"
mock_user_proxy = MagicMock()
# chat_result with no summary so extraction falls back to messages
mock_chat_result = MagicMock()
mock_chat_result.summary = None
mock_user_proxy.initiate_chat.return_value = mock_chat_result
mock_groupchat = MagicMock()
mock_groupchat.messages = messages
mock_manager = MagicMock()
with patch("autogen.LLMConfig", create=True, return_value=mock_llm_config), \
patch("autogen.AssistantAgent", create=True, return_value=mock_assistant), \
patch("autogen.UserProxyAgent", create=True, return_value=mock_user_proxy), \
patch("autogen.GroupChat", create=True, return_value=mock_groupchat), \
patch("autogen.GroupChatManager", create=True, return_value=mock_manager):
return gen._run_ag2(config, "Test", {})
def test_output_prefixed_with_ag2_header(self):
"""Result always starts with '### AG2 Output ###'."""
result = self._run_with_messages([
{"name": "Researcher", "content": "Here are my findings. TERMINATE", "role": "assistant"}
])
assert result.startswith("### AG2 Output ###")
def test_terminate_marker_stripped_from_output(self):
"""TERMINATE keyword is removed from the extracted output."""
result = self._run_with_messages([
{"name": "Researcher", "content": "Detailed findings here. TERMINATE", "role": "assistant"}
])
assert "TERMINATE" not in result
assert "Detailed findings here" in result
def test_user_messages_skipped_in_extraction(self):
"""User proxy messages are not included in the extracted output."""
result = self._run_with_messages([
{"name": "User", "content": "This is the user message", "role": "user"},
{"name": "Researcher", "content": "This is the agent response. TERMINATE", "role": "assistant"},
])
assert "This is the user message" not in result
assert "This is the agent response" in result
def test_execution_error_returns_error_message(self):
"""Exception during initiate_chat returns a '### AG2 Error ###' message."""
gen = self._make_gen()
config = _make_config()
mock_llm_config = MagicMock()
mock_llm_config.__enter__ = MagicMock(return_value=mock_llm_config)
mock_llm_config.__exit__ = MagicMock(return_value=False)
mock_assistant = MagicMock()
mock_assistant.name = "Researcher"
mock_user_proxy = MagicMock()
mock_user_proxy.initiate_chat.side_effect = RuntimeError("Connection failed")
mock_groupchat = MagicMock()
mock_groupchat.messages = []
mock_manager = MagicMock()
with patch("autogen.LLMConfig", create=True, return_value=mock_llm_config), \
patch("autogen.AssistantAgent", create=True, return_value=mock_assistant), \
patch("autogen.UserProxyAgent", create=True, return_value=mock_user_proxy), \
patch("autogen.GroupChat", create=True, return_value=mock_groupchat), \
patch("autogen.GroupChatManager", create=True, return_value=mock_manager):
result = gen._run_ag2(config, "Test", {})
assert "### AG2 Error ###" in result
assert "Connection failed" in result
if __name__ == "__main__":
pytest.main([__file__, "-v"])
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""
Test script for Claude Code integration with PraisonAI Agents
This script tests the basic functionality of the refactored Claude Code integration
"""
import os
import sys
import asyncio
import unittest
# Add the src directory to the path for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src', 'praisonai'))
def test_claude_code_tool_import():
"""Test that we can import the Claude Code tool function"""
try:
from praisonai.ui.code import claude_code_tool
print("✅ Successfully imported claude_code_tool")
return True
except ImportError as e:
print(f"❌ Failed to import claude_code_tool: {e}")
return False
def test_praisonai_agents_import():
"""Test praisonaiagents import availability"""
try:
from praisonaiagents import Agent
print("✅ PraisonAI Agents is available")
return True
except ImportError as e:
print(f"⚠️ PraisonAI Agents not available (fallback to litellm): {e}")
return False
def test_claude_code_availability():
"""Test if Claude Code CLI is available"""
import subprocess
try:
result = subprocess.run(['claude', '--version'], capture_output=True, text=True, timeout=10)
if result.returncode == 0:
print(f"✅ Claude Code CLI is available: {result.stdout.strip()}")
return True
else:
print(f"❌ Claude Code CLI returned error: {result.stderr}")
return False
except FileNotFoundError:
print("⚠️ Claude Code CLI not found in PATH")
return False
except subprocess.TimeoutExpired:
print("⚠️ Claude Code CLI check timed out")
return False
except Exception as e:
print(f"⚠️ Error checking Claude Code CLI: {e}")
return False
async def test_claude_code_tool_execution():
"""Test basic Claude Code tool execution (simple query)"""
try:
from praisonai.ui.code import claude_code_tool
# Test with a simple query that shouldn't modify files
test_query = "What is the current directory?"
result = await claude_code_tool(test_query)
print(f"✅ Claude Code tool executed successfully")
print(f"Query: {test_query}")
print(f"Result (first 100 chars): {str(result)[:100]}...")
return True
except Exception as e:
print(f"❌ Claude Code tool execution failed: {e}")
return False
def test_environment_variables():
"""Test that environment variables are properly set"""
test_vars = [
"PRAISONAI_CLAUDECODE_ENABLED",
"PRAISONAI_CODE_REPO_PATH"
]
print("Environment Variables:")
for var in test_vars:
value = os.getenv(var, "NOT_SET")
print(f" {var}: {value}")
return True
async def run_tests():
"""Run all tests"""
print("🧪 Testing Claude Code Integration with PraisonAI Agents")
print("=" * 60)
tests = [
("Import Claude Code Tool", test_claude_code_tool_import),
("Import PraisonAI Agents", test_praisonai_agents_import),
("Environment Variables", test_environment_variables),
("Claude Code CLI Availability", test_claude_code_availability),
]
results = {}
for test_name, test_func in tests:
print(f"\n🔍 {test_name}:")
try:
results[test_name] = test_func()
except Exception as e:
print(f"❌ Test failed with exception: {e}")
results[test_name] = False
# Only test Claude Code tool execution if CLI is available
if results.get("Claude Code CLI Availability", False):
print(f"\n🔍 Claude Code Tool Execution:")
try:
results["Claude Code Tool Execution"] = await test_claude_code_tool_execution()
except Exception as e:
print(f"❌ Test failed with exception: {e}")
results["Claude Code Tool Execution"] = False
# Summary
print("\n" + "=" * 60)
print("📊 Test Summary:")
passed = sum(1 for result in results.values() if result)
total = len(results)
for test_name, result in results.items():
status = "✅ PASS" if result else "❌ FAIL"
print(f" {status} {test_name}")
print(f"\nResults: {passed}/{total} tests passed")
if passed == total:
print("🎉 All tests passed! Integration is working correctly.")
else:
print("⚠️ Some tests failed. Please check the implementation.")
return passed == total
if __name__ == "__main__":
asyncio.run(run_tests())
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""
Quick test for the remote agent connectivity feature.
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src', 'praisonai-agents'))
from praisonaiagents import Session
def test_remote_session_creation():
"""Test creating a remote session."""
print("🧪 Testing remote session creation...")
try:
# This should fail gracefully since there's no server running
session = Session(agent_url="localhost:8000/agent")
print(f"❌ Unexpected: Session created without server: {session}")
except ConnectionError as e:
print(f"✅ Expected connection error: {e}")
except Exception as e:
print(f"❌ Unexpected error: {e}")
return False
return True
def test_local_session_backwards_compatibility():
"""Test that local sessions still work as before."""
print("\n🧪 Testing local session backwards compatibility...")
try:
# This should work as before
session = Session(session_id="test_local")
print(f"✅ Local session created: {session}")
# Test that it's not remote
if session.is_remote:
print("❌ Local session incorrectly marked as remote")
return False
print("✅ Local session correctly identified")
return True
except Exception as e:
print(f"❌ Error creating local session: {e}")
return False
def test_remote_session_restrictions():
"""Test that remote sessions properly restrict local operations."""
print("\n🧪 Testing remote session restrictions...")
try:
# Create a mock remote session (will fail connection but that's OK for testing restrictions)
session = Session(agent_url="mock://fake-server:8000/agent")
except ConnectionError:
# Create the session object manually for testing restrictions
session = Session.__new__(Session)
session.session_id = "test_remote"
session.user_id = "test_user"
session.agent_url = "mock://fake-server:8000/agent"
session.timeout = 30
session.is_remote = True
session.memory_config = {}
session.knowledge_config = {}
session._memory = None
session._knowledge = None
session._agents_instance = None
# Test memory restriction
try:
_ = session.memory
print("❌ Memory access should be restricted for remote sessions")
return False
except ValueError as e:
print(f"✅ Memory properly restricted: {e}")
# Test knowledge restriction
try:
_ = session.knowledge
print("❌ Knowledge access should be restricted for remote sessions")
return False
except ValueError as e:
print(f"✅ Knowledge properly restricted: {e}")
# Test agent creation restriction
try:
session.Agent(name="Test", role="Assistant")
print("❌ Agent creation should be restricted for remote sessions")
return False
except ValueError as e:
print(f"✅ Agent creation properly restricted: {e}")
return True
def main():
"""Run all tests."""
print("🚀 Testing Remote Agent Connectivity Implementation")
print("=" * 60)
tests = [
test_remote_session_creation,
test_local_session_backwards_compatibility,
test_remote_session_restrictions
]
passed = 0
total = len(tests)
for test in tests:
if test():
passed += 1
else:
print("❌ Test failed!")
print("\n" + "=" * 60)
print(f"📊 Test Results: {passed}/{total} tests passed")
if passed == total:
print("🎉 All tests passed! Remote agent connectivity is working correctly.")
return True
else:
print("⚠️ Some tests failed. Check the implementation.")
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)
+6 -5
View File
@@ -3931,7 +3931,7 @@ wheels = [
[[package]]
name = "praisonai"
version = "2.2.30"
version = "2.2.33"
source = { editable = "." }
dependencies = [
{ name = "instructor" },
@@ -4073,7 +4073,7 @@ requires-dist = [
{ name = "plotly", marker = "extra == 'realtime'", specifier = ">=5.24.0" },
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.15" },
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.15" },
{ name = "praisonaiagents", specifier = ">=0.0.102" },
{ name = "praisonaiagents", specifier = ">=0.0.106" },
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = ">=0.2.19" },
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
@@ -4130,17 +4130,18 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.102"
version = "0.0.106"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mcp" },
{ name = "openai" },
{ name = "posthog" },
{ name = "pydantic" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/e5/6f89d32b022f86561a9ffd5ceba5e8bcd29a531396c19a591ac12efdbf3b/praisonaiagents-0.0.102.tar.gz", hash = "sha256:c51ef93663a4d46c09c956b34a291fbb2bfefc07f546add587c441f8d801113a", size = 145079 }
sdist = { url = "https://files.pythonhosted.org/packages/81/9a/7b488726d34ebf7f98c27dd3e730ed0ca14dec7e63f9cb8fe4129aaf0e24/praisonaiagents-0.0.106.tar.gz", hash = "sha256:0fecb0df6c1ace618374a3d78eb6f03a058e70c606449069363cda0505a59487", size = 154663 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/e4/2ddc2b3a38e118454ee6c047e08b2f0baea3fcf45fe85cffa19221e79968/praisonaiagents-0.0.102-py3-none-any.whl", hash = "sha256:52dfe9d15a0a0c6d44194e45d91fe513e5633164f3ff6426c41068c302c5ff7a", size = 165203 },
{ url = "https://files.pythonhosted.org/packages/3f/75/6d56dbb2a03a4d0160ba6f3ebe60b468e3ee859f31c2833ac9d03130bdaf/praisonaiagents-0.0.106-py3-none-any.whl", hash = "sha256:504c4e11558bc21b8757efba8211b231c7ee235c46cbaafe02038fa493124f83", size = 175597 },
]
[[package]]