Skip to content

Commit ff3ca30

Browse files
committed
Release v3.10.12
1 parent 6f1f4a5 commit ff3ca30

29 files changed

+1105
-139
lines changed

docker/Dockerfile.chat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
1616
# Install Python packages (using latest versions)
1717
RUN pip install --no-cache-dir \
1818
praisonai_tools \
19-
"praisonai>=3.10.11" \
19+
"praisonai>=3.10.12" \
2020
"praisonai[chat]" \
2121
"embedchain[github,youtube]"
2222

docker/Dockerfile.dev

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
2020
# Install Python packages (using latest versions)
2121
RUN pip install --no-cache-dir \
2222
praisonai_tools \
23-
"praisonai>=3.10.11" \
23+
"praisonai>=3.10.12" \
2424
"praisonai[ui]" \
2525
"praisonai[chat]" \
2626
"praisonai[realtime]" \

docker/Dockerfile.ui

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
1616
# Install Python packages (using latest versions)
1717
RUN pip install --no-cache-dir \
1818
praisonai_tools \
19-
"praisonai>=3.10.11" \
19+
"praisonai>=3.10.12" \
2020
"praisonai[ui]" \
2121
"praisonai[crewai]"
2222

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Performance Benchmark CI
2+
# Runs import time and instantiation benchmarks on push/PR
3+
# Fails if import time exceeds 200ms threshold
4+
5+
name: Performance Benchmark
6+
7+
on:
8+
push:
9+
branches: [main, develop]
10+
paths:
11+
- 'praisonaiagents/**'
12+
- 'benchmarks/**'
13+
- '.github/workflows/benchmark.yml'
14+
pull_request:
15+
branches: [main, develop]
16+
paths:
17+
- 'praisonaiagents/**'
18+
- 'benchmarks/**'
19+
- '.github/workflows/benchmark.yml'
20+
workflow_dispatch: # Allow manual trigger
21+
22+
jobs:
23+
benchmark:
24+
runs-on: ubuntu-latest
25+
timeout-minutes: 10
26+
27+
steps:
28+
- uses: actions/checkout@v4
29+
30+
- name: Set up Python
31+
uses: actions/setup-python@v5
32+
with:
33+
python-version: '3.11'
34+
cache: 'pip'
35+
36+
- name: Install package
37+
run: |
38+
pip install -e .
39+
pip install pytest
40+
41+
- name: Measure Import Time
42+
id: import_time
43+
run: |
44+
# Measure import time (full Agent import)
45+
IMPORT_TIME=$(python -c "
46+
import time
47+
import sys
48+
49+
# Clear any cached modules
50+
for key in list(sys.modules.keys()):
51+
if 'praisonai' in key:
52+
del sys.modules[key]
53+
54+
start = time.perf_counter()
55+
from praisonaiagents import Agent
56+
elapsed = (time.perf_counter() - start) * 1000
57+
print(f'{elapsed:.1f}')
58+
")
59+
60+
echo "import_time=$IMPORT_TIME" >> $GITHUB_OUTPUT
61+
echo "Import time: ${IMPORT_TIME}ms"
62+
63+
- name: Check Import Time Threshold
64+
run: |
65+
IMPORT_TIME=${{ steps.import_time.outputs.import_time }}
66+
THRESHOLD=200
67+
68+
echo "Import time: ${IMPORT_TIME}ms"
69+
echo "Threshold: ${THRESHOLD}ms"
70+
71+
# Compare using bc for floating point
72+
if (( $(echo "$IMPORT_TIME > $THRESHOLD" | bc -l) )); then
73+
echo "❌ FAIL: Import time ${IMPORT_TIME}ms exceeds threshold ${THRESHOLD}ms"
74+
exit 1
75+
else
76+
echo "✅ PASS: Import time ${IMPORT_TIME}ms is within threshold ${THRESHOLD}ms"
77+
fi
78+
79+
- name: Measure Instantiation Time
80+
run: |
81+
python -c "
82+
import time
83+
from praisonaiagents import Agent
84+
85+
# Warmup
86+
for _ in range(5):
87+
Agent(name='Test', output='silent')
88+
89+
# Measure
90+
times = []
91+
for _ in range(50):
92+
start = time.perf_counter()
93+
Agent(name='Test', output='silent')
94+
times.append((time.perf_counter() - start) * 1e6)
95+
96+
avg = sum(times) / len(times)
97+
min_t = min(times)
98+
max_t = max(times)
99+
100+
print(f'Instantiation time:')
101+
print(f' Average: {avg:.2f}μs')
102+
print(f' Min: {min_t:.2f}μs')
103+
print(f' Max: {max_t:.2f}μs')
104+
"
105+
106+
- name: Verify Lazy Imports
107+
run: |
108+
python -c "
109+
import sys
110+
111+
# Clear any cached modules
112+
for key in list(sys.modules.keys()):
113+
if 'praisonai' in key or 'litellm' in key:
114+
del sys.modules[key]
115+
116+
# Import package
117+
import praisonaiagents
118+
119+
# Check heavy deps are NOT loaded
120+
heavy_deps = ['litellm', 'chromadb', 'mem0', 'rich']
121+
loaded = []
122+
for dep in heavy_deps:
123+
if dep in sys.modules:
124+
loaded.append(dep)
125+
126+
if loaded:
127+
print(f'⚠️ WARNING: Heavy deps loaded eagerly: {loaded}')
128+
# Don't fail, just warn - some deps may be needed
129+
else:
130+
print('✅ All heavy deps are lazy loaded')
131+
"
132+
133+
- name: Run Quick Benchmark (if available)
134+
continue-on-error: true
135+
run: |
136+
if [ -f "benchmarks/quick_benchmark.py" ]; then
137+
python benchmarks/quick_benchmark.py --fast || true
138+
else
139+
echo "Quick benchmark not found, skipping"
140+
fi

src/praisonai-agents/benchmarks/quick_benchmark.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,20 @@
2929
# BENCHMARK FUNCTIONS
3030
# ============================================================================
3131

32-
def measure_import_time(module_name, clear_cache=True):
33-
"""Measure import time for a module."""
32+
def measure_import_time(module_name, clear_cache=True, full_agent_import=True):
33+
"""Measure import time for a module.
34+
35+
Args:
36+
module_name: Name of the module to import
37+
clear_cache: Whether to clear module cache before import
38+
full_agent_import: If True, measure full Agent class import (recommended).
39+
If False, measure package import only (misleading for Agno).
40+
41+
Note: Agno's main __init__.py only exports __version__, so measuring
42+
'import agno' gives misleading results (5ms). The actual Agent import
43+
'from agno.agent import Agent' takes ~500ms. This function measures
44+
the full Agent import by default for accurate comparison.
45+
"""
3446
if clear_cache:
3547
# Clear module cache
3648
mods_to_remove = [k for k in sys.modules.keys() if module_name.split('.')[0] in k]
@@ -41,9 +53,15 @@ def measure_import_time(module_name, clear_cache=True):
4153
start = time.perf_counter()
4254

4355
if module_name == 'praisonaiagents':
44-
import praisonaiagents
56+
if full_agent_import:
57+
from praisonaiagents import Agent # noqa: F401
58+
else:
59+
import praisonaiagents # noqa: F401
4560
elif module_name == 'agno':
46-
import agno
61+
if full_agent_import:
62+
from agno.agent import Agent # noqa: F401
63+
else:
64+
import agno # noqa: F401
4765

4866
end = time.perf_counter()
4967
return (end - start) * 1000 # Return in milliseconds

src/praisonai-agents/praisonaiagents/__init__.py

Lines changed: 99 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -45,43 +45,25 @@
4545
from .tools.base import BaseTool, ToolResult, ToolValidationError, validate_tool
4646
from .tools.decorator import tool, FunctionTool
4747
from .tools.registry import get_registry, register_tool, get_tool, ToolRegistry
48-
from .db import db
49-
from .obs import obs
48+
# db and obs are lazy-loaded via __getattr__ for performance
5049

5150
# Sub-packages for organized imports (pa.config, pa.tools, etc.)
5251
# These enable: import praisonaiagents as pa; pa.config.MemoryConfig
5352
from . import config
5453
from . import tools
55-
from . import memory
56-
from . import workflows
57-
# Note: knowledge and mcp are lazy-loaded via __getattr__ due to heavy deps
58-
59-
# Embedding API - explicit import shadows subpackage, enabling:
60-
# from praisonaiagents import embedding, embeddings, EmbeddingResult, get_dimensions
61-
# Note: litellm is still lazy-loaded INSIDE the functions (no performance impact)
62-
from .embedding.embed import embedding, aembedding
63-
from .embedding.result import EmbeddingResult
64-
from .embedding.dimensions import get_dimensions
65-
embeddings = embedding # Plural alias (OpenAI style: client.embeddings.create)
66-
aembeddings = aembedding # Plural alias for async
67-
68-
# Workflows - lightweight module
69-
from .workflows import (
70-
Workflow, WorkflowStep, WorkflowContext, StepResult,
71-
Route, Parallel, Loop, Repeat,
72-
route, parallel, loop, repeat,
73-
Pipeline # Alias for Workflow
74-
)
54+
# Note: db, obs, knowledge and mcp are lazy-loaded via __getattr__ due to heavy deps
55+
56+
# Embedding API - LAZY LOADED via __getattr__ for performance
57+
# Supports: embedding, embeddings, aembedding, aembeddings, EmbeddingResult, get_dimensions
58+
59+
# Workflows - LAZY LOADED (moved to __getattr__)
60+
# Workflow, WorkflowStep, WorkflowContext, StepResult, Route, Parallel, Loop, Repeat, etc.
61+
7562
# Guardrails - LAZY LOADED (imports main.py which imports rich)
7663
# GuardrailResult and LLMGuardrail moved to __getattr__
7764

78-
# Handoff - lightweight (unified agent-to-agent transfer)
79-
from .agent.handoff import (
80-
Handoff, handoff, handoff_filters,
81-
RECOMMENDED_PROMPT_PREFIX, prompt_with_handoff_instructions,
82-
HandoffConfig, HandoffResult, HandoffInputData,
83-
ContextPolicy, HandoffError, HandoffCycleError, HandoffDepthError, HandoffTimeoutError,
84-
)
65+
# Handoff - LAZY LOADED (moved to __getattr__)
66+
# Handoff, handoff, handoff_filters, etc.
8567

8668
# Flow display - LAZY LOADED (moved to __getattr__)
8769
# FlowDisplay and track_workflow are now lazy loaded
@@ -140,6 +122,94 @@ def __getattr__(name):
140122
_lazy_cache[name] = value
141123
return value
142124

125+
# Workflows - lazy loaded for performance
126+
_workflow_names = {
127+
'Workflow', 'WorkflowStep', 'WorkflowContext', 'StepResult',
128+
'Route', 'Parallel', 'Loop', 'Repeat',
129+
'route', 'parallel', 'loop', 'repeat', 'Pipeline'
130+
}
131+
if name in _workflow_names:
132+
from . import workflows as _workflows_module
133+
value = getattr(_workflows_module, name)
134+
_lazy_cache[name] = value
135+
return value
136+
137+
# Handoff - lazy loaded for performance
138+
_handoff_names = {
139+
'Handoff', 'handoff', 'handoff_filters',
140+
'RECOMMENDED_PROMPT_PREFIX', 'prompt_with_handoff_instructions',
141+
'HandoffConfig', 'HandoffResult', 'HandoffInputData',
142+
'ContextPolicy', 'HandoffError', 'HandoffCycleError',
143+
'HandoffDepthError', 'HandoffTimeoutError'
144+
}
145+
if name in _handoff_names:
146+
# Import directly from handoff module to avoid recursion
147+
from .agent.handoff import (
148+
Handoff, handoff, handoff_filters,
149+
RECOMMENDED_PROMPT_PREFIX, prompt_with_handoff_instructions,
150+
HandoffConfig, HandoffResult, HandoffInputData,
151+
ContextPolicy, HandoffError, HandoffCycleError,
152+
HandoffDepthError, HandoffTimeoutError
153+
)
154+
_handoff_exports = {
155+
'Handoff': Handoff, 'handoff': handoff, 'handoff_filters': handoff_filters,
156+
'RECOMMENDED_PROMPT_PREFIX': RECOMMENDED_PROMPT_PREFIX,
157+
'prompt_with_handoff_instructions': prompt_with_handoff_instructions,
158+
'HandoffConfig': HandoffConfig, 'HandoffResult': HandoffResult,
159+
'HandoffInputData': HandoffInputData, 'ContextPolicy': ContextPolicy,
160+
'HandoffError': HandoffError, 'HandoffCycleError': HandoffCycleError,
161+
'HandoffDepthError': HandoffDepthError, 'HandoffTimeoutError': HandoffTimeoutError
162+
}
163+
for k, v in _handoff_exports.items():
164+
_lazy_cache[k] = v
165+
return _handoff_exports[name]
166+
167+
# db and obs - lazy loaded for performance
168+
if name == 'db':
169+
from .db import db
170+
_lazy_cache[name] = db
171+
return db
172+
elif name == 'obs':
173+
from .obs import obs
174+
_lazy_cache[name] = obs
175+
return obs
176+
177+
# memory module - lazy loaded for performance
178+
if name == 'memory':
179+
import importlib
180+
_memory_module = importlib.import_module('.memory', __name__)
181+
_lazy_cache[name] = _memory_module
182+
return _memory_module
183+
184+
# workflows module - lazy loaded for performance
185+
if name == 'workflows':
186+
import importlib
187+
_workflows_module = importlib.import_module('.workflows', __name__)
188+
_lazy_cache[name] = _workflows_module
189+
return _workflows_module
190+
191+
# Embedding API - lazy loaded for performance
192+
_embedding_names = {'embedding', 'embeddings', 'aembedding', 'aembeddings', 'EmbeddingResult', 'get_dimensions'}
193+
if name in _embedding_names:
194+
if name in ('embedding', 'embeddings'):
195+
from .embedding.embed import embedding
196+
_lazy_cache['embedding'] = embedding
197+
_lazy_cache['embeddings'] = embedding
198+
return embedding
199+
elif name in ('aembedding', 'aembeddings'):
200+
from .embedding.embed import aembedding
201+
_lazy_cache['aembedding'] = aembedding
202+
_lazy_cache['aembeddings'] = aembedding
203+
return aembedding
204+
elif name == 'EmbeddingResult':
205+
from .embedding.result import EmbeddingResult
206+
_lazy_cache[name] = EmbeddingResult
207+
return EmbeddingResult
208+
elif name == 'get_dimensions':
209+
from .embedding.dimensions import get_dimensions
210+
_lazy_cache[name] = get_dimensions
211+
return get_dimensions
212+
143213
# Guardrails - lazy loaded to avoid importing main.py which imports rich
144214
if name == 'GuardrailResult':
145215
from .guardrails import GuardrailResult

0 commit comments

Comments
 (0)