Skip to content

Commit 8c39769

Browse files
committed
Release v3.9.35
1 parent 93904f8 commit 8c39769

File tree

14 files changed

+201
-44
lines changed

14 files changed

+201
-44
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.9.34" \
19+
"praisonai>=3.9.35" \
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.9.34" \
23+
"praisonai>=3.9.35" \
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.9.34" \
19+
"praisonai>=3.9.35" \
2020
"praisonai[ui]" \
2121
"praisonai[crewai]"
2222

src/praisonai-agents/praisonaiagents/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,15 @@ def _stub(*args, **kwargs):
534534
_lazy_cache[name] = result
535535
return result
536536

537-
# Context management config (already exists)
537+
# Context management config
538+
elif name == "ContextConfig":
539+
from .context.models import ContextConfig
540+
_lazy_cache[name] = ContextConfig
541+
return ContextConfig
542+
elif name == "OptimizerStrategy":
543+
from .context.models import OptimizerStrategy
544+
_lazy_cache[name] = OptimizerStrategy
545+
return OptimizerStrategy
538546
elif name == "ManagerConfig":
539547
from .context.manager import ManagerConfig
540548
_lazy_cache[name] = ManagerConfig

src/praisonai-agents/praisonaiagents/agent/agent.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,12 +1220,38 @@ def context_manager(self):
12201220
agent_name=self.name or "Agent",
12211221
)
12221222
elif isinstance(self._context_param, ManagerConfig):
1223-
# Use provided config
1223+
# Use provided ManagerConfig
12241224
self._context_manager = ContextManager(
12251225
model=self.llm if isinstance(self.llm, str) else "gpt-4o-mini",
12261226
config=self._context_param,
12271227
agent_name=self.name or "Agent",
12281228
)
1229+
elif hasattr(self._context_param, 'auto_compact') and hasattr(self._context_param, 'tool_output_max'):
1230+
# ContextConfig from YAML - convert to ManagerConfig
1231+
try:
1232+
from ..context.models import ContextConfig as _ContextConfig
1233+
if isinstance(self._context_param, _ContextConfig):
1234+
# Build ManagerConfig from ContextConfig fields
1235+
manager_config = ManagerConfig(
1236+
auto_compact=self._context_param.auto_compact,
1237+
compact_threshold=self._context_param.compact_threshold,
1238+
strategy=self._context_param.strategy,
1239+
output_reserve=self._context_param.output_reserve,
1240+
default_tool_output_max=self._context_param.tool_output_max, # Map field name
1241+
protected_tools=list(self._context_param.protected_tools),
1242+
keep_recent_turns=self._context_param.keep_recent_turns,
1243+
monitor_enabled=self._context_param.monitor.enabled if self._context_param.monitor else False,
1244+
)
1245+
self._context_manager = ContextManager(
1246+
model=self.llm if isinstance(self.llm, str) else "gpt-4o-mini",
1247+
config=manager_config,
1248+
agent_name=self.name or "Agent",
1249+
)
1250+
else:
1251+
self._context_manager = None
1252+
except Exception as e:
1253+
logging.debug(f"ContextConfig conversion failed: {e}")
1254+
self._context_manager = None
12291255
elif hasattr(self._context_param, 'process'):
12301256
# Already a ContextManager instance
12311257
self._context_manager = self._context_param
@@ -2920,11 +2946,28 @@ def execute_tool(self, function_name, arguments):
29202946
return self._execute_tool_with_context(function_name, arguments, state)
29212947

29222948
def _execute_tool_with_context(self, function_name, arguments, state):
2923-
"""Execute tool within injection context."""
2949+
"""Execute tool within injection context, with optional output truncation."""
29242950
from ..tools.injected import with_injection_context
29252951

29262952
with with_injection_context(state):
2927-
return self._execute_tool_impl(function_name, arguments)
2953+
result = self._execute_tool_impl(function_name, arguments)
2954+
2955+
# Apply context-aware truncation if context management is enabled
2956+
# This prevents tool outputs (e.g., search results) from exploding the context
2957+
if self.context_manager and result:
2958+
try:
2959+
truncated = self._truncate_tool_output(function_name, str(result))
2960+
if len(truncated) < len(str(result)):
2961+
logging.debug(f"Truncated {function_name} output from {len(str(result))} to {len(truncated)} chars")
2962+
# Return truncated string if significantly shorter
2963+
if isinstance(result, dict):
2964+
# Keep dict structure but may have truncated string representation
2965+
return result # Let the string version be truncated at display time
2966+
return truncated
2967+
except Exception as e:
2968+
logging.debug(f"Tool truncation skipped: {e}")
2969+
2970+
return result
29282971

29292972
def _execute_tool_impl(self, function_name, arguments):
29302973
"""Internal tool execution implementation."""

src/praisonai-agents/praisonaiagents/workflows/workflows.py

Lines changed: 114 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import os
2323
import re
24+
import json
2425
import logging
2526
from pathlib import Path
2627
from typing import Any, Dict, List, Optional, Callable, Tuple, Union
@@ -34,6 +35,49 @@
3435
logger = logging.getLogger(__name__)
3536

3637

38+
def _parse_json_output(output: Any, step_name: str = "step") -> Any:
39+
"""
40+
Parse JSON from LLM output if it's a string.
41+
42+
Handles:
43+
- Direct JSON strings: '{"key": "value"}'
44+
- Markdown code blocks: ```json\n{"key": "value"}\n```
45+
46+
Returns:
47+
Parsed dict/list if successful, original output otherwise
48+
"""
49+
if not isinstance(output, str) or not output:
50+
return output
51+
52+
# Try direct JSON parse first
53+
try:
54+
return json.loads(output)
55+
except json.JSONDecodeError:
56+
pass
57+
58+
# Try extracting from markdown code block
59+
json_match = re.search(r'```(?:json)?\s*\n?([\s\S]*?)\n?```', output)
60+
if json_match:
61+
try:
62+
return json.loads(json_match.group(1).strip())
63+
except json.JSONDecodeError:
64+
pass
65+
66+
# Try finding JSON object/array in text
67+
# Look for {...} or [...]
68+
for pattern in [r'(\{[^}]+\})', r'(\[[^\]]+\])']:
69+
match = re.search(pattern, output)
70+
if match:
71+
try:
72+
return json.loads(match.group(1))
73+
except json.JSONDecodeError:
74+
continue
75+
76+
# Return original if can't parse
77+
logger.debug(f"Could not parse JSON from step '{step_name}' output")
78+
return output
79+
80+
3781
@dataclass
3882
class WorkflowContext:
3983
"""Context passed to step handlers. Contains all information about the current workflow state."""
@@ -1206,6 +1250,10 @@ def run(
12061250

12071251
output = step.agent.chat(action, **chat_kwargs)
12081252

1253+
# Parse JSON output if output_json was requested and output is a string
1254+
if step_output_json and output and isinstance(output, str):
1255+
output = _parse_json_output(output, step.name)
1256+
12091257
# Handle output_pydantic if present
12101258
output_pydantic = getattr(step, 'output_pydantic', None)
12111259
if output_pydantic and output:
@@ -1257,6 +1305,11 @@ def run(
12571305

12581306
output = temp_agent.chat(action, stream=stream)
12591307

1308+
# Parse JSON output if output_json was requested
1309+
step_output_json = getattr(step, '_output_json', None)
1310+
if step_output_json and output and isinstance(output, str):
1311+
output = _parse_json_output(output, step.name)
1312+
12601313
except Exception as e:
12611314
step_error = e
12621315
output = f"Error: {e}"
@@ -1342,6 +1395,26 @@ def run(
13421395
var_name = step.output_variable or f"{step.name}_output"
13431396
all_variables[var_name] = output
13441397

1398+
# Validate output and warn about issues
1399+
if output is None:
1400+
logger.warning(f"⚠️ Step '{step.name}': Output is None. Agent may not have returned expected format.")
1401+
if verbose:
1402+
print(f"⚠️ WARNING: Step '{step.name}' output is None!")
1403+
else:
1404+
# Check type against output_json schema if defined
1405+
expected_schema = getattr(step, '_output_json', None)
1406+
if expected_schema and isinstance(expected_schema, dict):
1407+
expected_type = expected_schema.get('type')
1408+
actual_type = type(output).__name__
1409+
if expected_type == 'object' and not isinstance(output, dict):
1410+
logger.warning(f"⚠️ Step '{step.name}': Expected object/dict, got {actual_type}")
1411+
if verbose:
1412+
print(f"⚠️ Step '{step.name}': Expected 'object', received '{actual_type}'")
1413+
elif expected_type == 'array' and not isinstance(output, list):
1414+
logger.warning(f"⚠️ Step '{step.name}': Expected array/list, got {actual_type}")
1415+
if verbose:
1416+
print(f"⚠️ Step '{step.name}': Expected 'array', received '{actual_type}'")
1417+
13451418
i += 1
13461419

13471420
# Update workflow status
@@ -1399,33 +1472,12 @@ async def arun(
13991472
return await self.astart(input, llm, verbose)
14001473

14011474
def _normalize_steps(self) -> List['WorkflowStep']:
1402-
"""Convert mixed steps (Agent, function, WorkflowStep) to WorkflowStep list."""
1403-
normalized = []
1404-
1405-
for i, step in enumerate(self.steps):
1406-
if isinstance(step, WorkflowStep):
1407-
normalized.append(step)
1408-
elif callable(step):
1409-
# It's a function - wrap as handler
1410-
normalized.append(WorkflowStep(
1411-
name=getattr(step, '__name__', f'step_{i+1}'),
1412-
handler=step
1413-
))
1414-
elif hasattr(step, 'chat'):
1415-
# It's an Agent - wrap with agent reference
1416-
normalized.append(WorkflowStep(
1417-
name=getattr(step, 'name', f'agent_{i+1}'),
1418-
agent=step,
1419-
action="{{input}}"
1420-
))
1421-
else:
1422-
# Unknown type - try to use as string action
1423-
normalized.append(WorkflowStep(
1424-
name=f'step_{i+1}',
1425-
action=str(step)
1426-
))
1475+
"""Convert mixed steps (Agent, function, WorkflowStep) to WorkflowStep list.
14271476
1428-
return normalized
1477+
This method uses _normalize_single_step to ensure consistent normalization
1478+
and avoid duplicated code paths (DRY principle).
1479+
"""
1480+
return [self._normalize_single_step(step, i) for i, step in enumerate(self.steps)]
14291481

14301482
def _create_plan(self, input: str, model: str, verbose: bool) -> Optional[str]:
14311483
"""Create an execution plan for the workflow using LLM.
@@ -1608,6 +1660,11 @@ def _execute_single_step_internal(
16081660
action = f"{action}\n\nContext from previous step:\n{previous_output}"
16091661
action = action.replace("{{input}}", input)
16101662
output = normalized.agent.chat(action, stream=stream)
1663+
1664+
# Parse JSON output if output_json was requested
1665+
step_output_json = getattr(normalized, '_output_json', None)
1666+
if step_output_json and output and isinstance(output, str):
1667+
output = _parse_json_output(output, normalized.name)
16111668
except Exception as e:
16121669
output = f"Error: {e}"
16131670
elif normalized.action:
@@ -1633,6 +1690,11 @@ def _execute_single_step_internal(
16331690
action = f"{action}\n\nContext from previous step:\n{previous_output}"
16341691

16351692
output = temp_agent.chat(action, stream=stream)
1693+
1694+
# Parse JSON output if output_json was requested
1695+
step_output_json = getattr(normalized, '_output_json', None)
1696+
if step_output_json and output and isinstance(output, str):
1697+
output = _parse_json_output(output, normalized.name)
16361698
except Exception as e:
16371699
output = f"Error: {e}"
16381700

@@ -1850,6 +1912,32 @@ def execute_item(idx_item_tuple):
18501912
all_variables["loop_outputs"] = outputs # Also keep for backward compatibility
18511913
combined_output = "\n".join(str(o) for o in outputs) if outputs else ""
18521914

1915+
# Validate outputs and warn about issues
1916+
none_count = sum(1 for o in outputs if o is None)
1917+
if none_count > 0:
1918+
logger.warning(f"⚠️ Loop '{output_var_name}': {none_count}/{len(outputs)} outputs are None. "
1919+
f"Check if agent returned expected format.")
1920+
if verbose:
1921+
print(f"⚠️ WARNING: {none_count}/{len(outputs)} loop outputs are None!")
1922+
1923+
# Check for type consistency
1924+
if outputs and len(outputs) > 0:
1925+
expected_type = loop_step.step._output_json if hasattr(loop_step.step, '_output_json') else None
1926+
if expected_type:
1927+
expected_schema_type = expected_type.get('type') if isinstance(expected_type, dict) else None
1928+
for i, o in enumerate(outputs):
1929+
if o is None:
1930+
continue
1931+
actual_type = type(o).__name__
1932+
if expected_schema_type == 'object' and not isinstance(o, dict):
1933+
logger.warning(f"⚠️ Loop output[{i}]: Expected object/dict, got {actual_type}")
1934+
if verbose:
1935+
print(f"⚠️ Loop output[{i}]: Expected 'object', received '{actual_type}'")
1936+
elif expected_schema_type == 'array' and not isinstance(o, list):
1937+
logger.warning(f"⚠️ Loop output[{i}]: Expected array/list, got {actual_type}")
1938+
if verbose:
1939+
print(f"⚠️ Loop output[{i}]: Expected 'array', received '{actual_type}'")
1940+
18531941
# Debug logging for output_variable
18541942
if verbose:
18551943
print(f"📦 Loop stored {len(outputs)} results in variable: '{output_var_name}'")

src/praisonai-agents/praisonaiagents/workflows/yaml_parser.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,23 @@ def _parse_workflow_data(self, data: Dict[str, Any]) -> Workflow:
294294
# Build memory config if specified - pass raw dict for flexibility
295295
memory_value = memory_config if memory_config else None
296296

297+
# Parse context management config (CRITICAL for token overflow prevention)
298+
# Supports: context: true OR context: {auto_compact: true, strategy: smart, ...}
299+
context_config = data.get('context')
300+
if context_config is True:
301+
# Simple enable: context: true
302+
context_value = True
303+
elif isinstance(context_config, dict):
304+
# Detailed config: context: {auto_compact: true, ...}
305+
try:
306+
from ..context.models import ContextConfig
307+
context_value = ContextConfig(**context_config)
308+
except Exception:
309+
# Fallback: just enable with True if ContextConfig fails
310+
context_value = True
311+
else:
312+
context_value = None
313+
297314
workflow = Workflow(
298315
name=name,
299316
steps=steps,
@@ -302,6 +319,7 @@ def _parse_workflow_data(self, data: Dict[str, Any]) -> Workflow:
302319
default_llm=default_llm,
303320
output=workflow_output, # Pass output mode to Workflow
304321
memory=memory_value, # Pass memory config to Workflow
322+
context=context_value, # Pass context management config to Workflow
305323
)
306324

307325
# Store additional attributes for feature parity with agents.yaml

src/praisonai-agents/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "praisonaiagents"
7-
version = "0.12.18"
7+
version = "0.12.20"
88
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
99
readme = "README.md"
1010
requires-python = ">=3.10"

src/praisonai-agents/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/praisonai/praisonai.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ class Praisonai < Formula
33

44
desc "AI tools for various AI applications"
55
homepage "https://github.com/MervinPraison/PraisonAI"
6-
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.9.34.tar.gz"
7-
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.9.34.tar.gz | shasum -a 256`.split.first
6+
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.9.35.tar.gz"
7+
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.9.35.tar.gz | shasum -a 256`.split.first
88
license "MIT"
99

1010
depends_on "python@3.11"

0 commit comments

Comments
 (0)