Skip to content

Commit 557e243

Browse files
committed
Release v3.10.15
1 parent 0628204 commit 557e243

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+4323
-1419
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.14" \
19+
"praisonai>=3.10.15" \
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.14" \
23+
"praisonai>=3.10.15" \
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.14" \
19+
"praisonai>=3.10.15" \
2020
"praisonai[ui]" \
2121
"praisonai[crewai]"
2222

src/praisonai-agents/praisonaiagents/config/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"MemoryConfig",
3030
"KnowledgeConfig",
3131
"PlanningConfig",
32+
"MultiAgentPlanningConfig",
3233
"ReflectionConfig",
3334
"GuardrailConfig",
3435
"WebConfig",
@@ -91,6 +92,7 @@
9192
"MemoryConfig": "feature_configs",
9293
"KnowledgeConfig": "feature_configs",
9394
"PlanningConfig": "feature_configs",
95+
"MultiAgentPlanningConfig": "feature_configs",
9496
"ReflectionConfig": "feature_configs",
9597
"GuardrailConfig": "feature_configs",
9698
"WebConfig": "feature_configs",

src/praisonai-agents/praisonaiagents/context/instrumentation.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ class ContextMetrics:
4646
# Token tracking
4747
tokens_processed: int = 0
4848
tokens_saved: int = 0
49+
tokens_saved_by_summarization: int = 0 # Tokens saved by LLM summarization
50+
tokens_saved_by_truncation: int = 0 # Tokens saved by truncation
4951
compactions_triggered: int = 0
52+
tool_outputs_summarized: int = 0 # Count of tool outputs summarized
53+
tool_outputs_truncated: int = 0 # Count of tool outputs truncated
5054

5155
# Cache stats
5256
cache_hits: int = 0
@@ -76,8 +80,14 @@ def to_dict(self) -> Dict[str, Any]:
7680
"tokens": {
7781
"processed": self.tokens_processed,
7882
"saved": self.tokens_saved,
83+
"saved_by_summarization": self.tokens_saved_by_summarization,
84+
"saved_by_truncation": self.tokens_saved_by_truncation,
7985
"compactions": self.compactions_triggered,
8086
},
87+
"tool_outputs": {
88+
"summarized": self.tool_outputs_summarized,
89+
"truncated": self.tool_outputs_truncated,
90+
},
8191
"cache": {
8292
"hits": self.cache_hits,
8393
"misses": self.cache_misses,

src/praisonai-agents/praisonaiagents/context/manager.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,67 @@ def clear(self) -> None:
9292
self._stats = {"duplicates_prevented": 0, "tokens_saved": 0}
9393

9494

95+
def deduplicate_topics(topics: list, key: str = "title", similarity_threshold: float = 0.8) -> list:
96+
"""
97+
Programmatic deduplication of topics/items before agent processing.
98+
99+
This helps prevent duplicate content from being passed to downstream agents,
100+
reducing token waste and improving quality.
101+
102+
Args:
103+
topics: List of topic dicts or strings
104+
key: Key to use for comparison if topics are dicts (default: "title")
105+
similarity_threshold: Similarity threshold for fuzzy matching (0.0-1.0)
106+
107+
Returns:
108+
Deduplicated list of topics
109+
"""
110+
if not topics:
111+
return topics
112+
113+
seen_hashes = set()
114+
seen_normalized = set()
115+
unique_topics = []
116+
117+
for topic in topics:
118+
# Get the content to compare
119+
if isinstance(topic, dict):
120+
content = str(topic.get(key, topic.get("content", str(topic))))
121+
else:
122+
content = str(topic)
123+
124+
# Normalize for comparison
125+
normalized = content.lower().strip()
126+
# Remove common words for better matching
127+
normalized = " ".join(w for w in normalized.split() if len(w) > 3)
128+
129+
# Check exact hash match
130+
content_hash = hashlib.md5(normalized.encode()).hexdigest()
131+
if content_hash in seen_hashes:
132+
continue
133+
134+
# Check fuzzy match using simple word overlap
135+
is_duplicate = False
136+
for seen in seen_normalized:
137+
# Calculate Jaccard similarity
138+
words1 = set(normalized.split())
139+
words2 = set(seen.split())
140+
if words1 and words2:
141+
intersection = len(words1 & words2)
142+
union = len(words1 | words2)
143+
similarity = intersection / union if union > 0 else 0
144+
if similarity >= similarity_threshold:
145+
is_duplicate = True
146+
break
147+
148+
if not is_duplicate:
149+
seen_hashes.add(content_hash)
150+
seen_normalized.add(normalized)
151+
unique_topics.append(topic)
152+
153+
return unique_topics
154+
155+
95156
class EstimationMode(str, Enum):
96157
"""Token estimation modes."""
97158
HEURISTIC = "heuristic"
@@ -256,6 +317,10 @@ class ManagerConfig:
256317
# LLM-powered summarization
257318
llm_summarize: bool = False # Enable LLM-powered summarization
258319

320+
# Smart tool output summarization
321+
smart_tool_summarize: bool = True # Summarize large tool outputs using LLM before truncating
322+
tool_summarize_limits: Dict[str, int] = field(default_factory=dict) # Per-tool min_chars_to_summarize
323+
259324
# Estimation
260325
estimation_mode: EstimationMode = EstimationMode.HEURISTIC
261326
log_estimation_mismatch: bool = False
@@ -306,6 +371,8 @@ def to_dict(self) -> Dict[str, Any]:
306371
"prune_after_tokens": self.prune_after_tokens,
307372
"keep_recent_turns": self.keep_recent_turns,
308373
"llm_summarize": self.llm_summarize,
374+
"smart_tool_summarize": self.smart_tool_summarize,
375+
"tool_summarize_limits": self.tool_summarize_limits,
309376
"source": self.source,
310377
}
311378
return result
@@ -617,6 +684,8 @@ def _optimize_with_benefit_check(
617684
preserve_recent=self.config.keep_recent_turns,
618685
protected_tools=self.config.protected_tools,
619686
llm_summarize_fn=self._llm_summarize_fn if self.config.llm_summarize else None,
687+
smart_tool_summarize=self.config.smart_tool_summarize,
688+
tool_summarize_limits=self.config.tool_summarize_limits,
620689
)
621690

622691
# Try optimization

src/praisonai-agents/praisonaiagents/context/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ class OptimizationResult:
214214
messages_removed: int = 0
215215
messages_tagged: int = 0
216216
tool_outputs_pruned: int = 0
217+
tool_outputs_summarized: int = 0 # Count of tool outputs summarized via LLM
218+
tokens_saved_by_summarization: int = 0 # Tokens saved specifically by LLM summarization
219+
tokens_saved_by_truncation: int = 0 # Tokens saved specifically by truncation
217220
summary_added: bool = False
218221

219222
@property
@@ -286,6 +289,9 @@ class ContextConfig:
286289
# LLM-powered summarization
287290
llm_summarize: bool = False # Enable LLM-powered summarization (uses agent's LLM)
288291

292+
# Smart tool output summarization (summarize before truncating)
293+
smart_tool_summarize: bool = True # Summarize large tool outputs using LLM before truncating
294+
289295
# Session tracking (Agno pattern)
290296
session_tracking: bool = False # Enable goal/plan/progress tracking
291297
track_summary: bool = True # Auto-extract conversation summary

src/praisonai-agents/praisonaiagents/context/optimizer.py

Lines changed: 148 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,109 @@ def _create_summary(self, messages: List[Dict[str, Any]]) -> str:
395395
return ""
396396

397397

398+
class SummarizeToolOutputsOptimizer(BaseOptimizer):
399+
"""
400+
Summarize large tool outputs using LLM before truncation.
401+
402+
This optimizer specifically targets tool role messages with large content,
403+
using an LLM to create intelligent summaries that preserve key information.
404+
Falls back to keeping original content if LLM is unavailable or fails.
405+
"""
406+
407+
def __init__(
408+
self,
409+
llm_summarize_fn: Optional[callable] = None,
410+
max_output_tokens: int = 1000,
411+
min_chars_to_summarize: int = 2000,
412+
preserve_recent: int = 2,
413+
tool_summarize_limits: Optional[Dict[str, int]] = None,
414+
):
415+
"""
416+
Initialize tool output summarizer.
417+
418+
Args:
419+
llm_summarize_fn: Function(content, max_tokens) -> summary string
420+
max_output_tokens: Target token count for summarized output
421+
min_chars_to_summarize: Default minimum chars before summarization triggers
422+
preserve_recent: Number of recent tool outputs to preserve intact
423+
tool_summarize_limits: Per-tool min_chars_to_summarize limits {tool_name: min_chars}
424+
"""
425+
self.llm_summarize_fn = llm_summarize_fn
426+
self.max_output_tokens = max_output_tokens
427+
self.min_chars_to_summarize = min_chars_to_summarize
428+
self.preserve_recent = preserve_recent
429+
self.tool_summarize_limits = tool_summarize_limits or {}
430+
431+
def optimize(
432+
self,
433+
messages: List[Dict[str, Any]],
434+
target_tokens: int,
435+
ledger: Optional[ContextLedger] = None,
436+
) -> tuple:
437+
original_tokens = estimate_messages_tokens(messages)
438+
439+
# If already under budget or no LLM function, return as-is
440+
if original_tokens <= target_tokens or not self.llm_summarize_fn:
441+
return messages, OptimizationResult(
442+
original_tokens=original_tokens,
443+
optimized_tokens=original_tokens,
444+
tokens_saved=0,
445+
strategy_used=OptimizerStrategy.SMART,
446+
)
447+
448+
result = []
449+
summarized_count = 0
450+
451+
# Find tool messages and their indices
452+
tool_indices = [i for i, m in enumerate(messages) if m.get("role") == "tool"]
453+
454+
# Preserve recent tool outputs (only if there are more than preserve_recent tools)
455+
if tool_indices and self.preserve_recent > 0 and len(tool_indices) > self.preserve_recent:
456+
recent_tool_indices = set(tool_indices[-self.preserve_recent:])
457+
else:
458+
recent_tool_indices = set() # Summarize all if few tools or preserve_recent=0
459+
460+
for i, msg in enumerate(messages):
461+
role = msg.get("role", "")
462+
content = msg.get("content", "")
463+
464+
# Only process tool messages with large content
465+
if role == "tool" and i not in recent_tool_indices:
466+
# Get per-tool limit or use default
467+
tool_name = msg.get("name", "")
468+
min_chars = self.tool_summarize_limits.get(tool_name, self.min_chars_to_summarize)
469+
if isinstance(content, str) and len(content) >= min_chars:
470+
# Try to summarize
471+
try:
472+
summary = self.llm_summarize_fn(content, self.max_output_tokens)
473+
if summary and len(summary) < len(content):
474+
summarized_msg = msg.copy()
475+
summarized_msg["content"] = summary
476+
summarized_msg["_summarized"] = True
477+
summarized_msg["_original_length"] = len(content)
478+
result.append(summarized_msg)
479+
summarized_count += 1
480+
continue
481+
except Exception:
482+
# Fallback to original on error
483+
pass
484+
485+
result.append(msg)
486+
487+
optimized_tokens = estimate_messages_tokens(result)
488+
489+
tokens_saved = original_tokens - optimized_tokens
490+
491+
return result, OptimizationResult(
492+
original_tokens=original_tokens,
493+
optimized_tokens=optimized_tokens,
494+
tokens_saved=tokens_saved,
495+
strategy_used=OptimizerStrategy.SMART,
496+
tool_outputs_summarized=summarized_count,
497+
tokens_saved_by_summarization=tokens_saved, # All savings from summarization
498+
)
499+
500+
398501
class LLMSummarizeOptimizer(SummarizeOptimizer):
399502
"""
400503
LLM-powered summarization optimizer.
@@ -475,9 +578,10 @@ class SmartOptimizer(BaseOptimizer):
475578
Smart optimization combining multiple strategies.
476579
477580
Applies strategies in order:
478-
1. Prune tool outputs
479-
2. Sliding window
480-
3. Summarize if still over
581+
1. Summarize tool outputs (if LLM available and smart_tool_summarize=True)
582+
2. Prune tool outputs (fallback truncation)
583+
3. Sliding window
584+
4. Summarize conversation if still over
481585
"""
482586

483587
def __init__(
@@ -486,11 +590,21 @@ def __init__(
486590
protected_tools: Optional[List[str]] = None,
487591
tool_limits: Optional[Dict[str, int]] = None,
488592
llm_summarize_fn: Optional[callable] = None,
593+
smart_tool_summarize: bool = True,
594+
tool_summarize_limits: Optional[Dict[str, int]] = None,
489595
):
490596
self.preserve_recent = preserve_recent
491597
self.protected_tools = protected_tools or []
492598
self.tool_limits = tool_limits or {}
599+
self.smart_tool_summarize = smart_tool_summarize
600+
self.tool_summarize_limits = tool_summarize_limits or {}
493601

602+
# Tool output summarization (LLM-powered, applied first when available)
603+
self._summarize_tools = SummarizeToolOutputsOptimizer(
604+
llm_summarize_fn=llm_summarize_fn if smart_tool_summarize else None,
605+
preserve_recent=preserve_recent,
606+
tool_summarize_limits=tool_summarize_limits,
607+
)
494608
self._prune = PruneToolsOptimizer(
495609
preserve_recent=preserve_recent,
496610
protected_tools=protected_tools,
@@ -518,19 +632,43 @@ def optimize(
518632
strategy_used=OptimizerStrategy.SMART,
519633
)
520634

521-
# Step 1: Prune tool outputs
522-
result, prune_result = self._prune.optimize(messages, target_tokens, ledger)
635+
# Step 1: Summarize tool outputs (LLM-powered, if available)
636+
tool_summarized_count = 0
637+
tokens_saved_by_summarization = 0
638+
if self._summarize_tools.llm_summarize_fn:
639+
result, tool_summary_result = self._summarize_tools.optimize(messages, target_tokens, ledger)
640+
tool_summarized_count = tool_summary_result.tool_outputs_summarized
641+
tokens_saved_by_summarization = tool_summary_result.tokens_saved_by_summarization
642+
643+
if estimate_messages_tokens(result) <= target_tokens:
644+
return result, OptimizationResult(
645+
original_tokens=original_tokens,
646+
optimized_tokens=tool_summary_result.optimized_tokens,
647+
tokens_saved=original_tokens - tool_summary_result.optimized_tokens,
648+
strategy_used=OptimizerStrategy.SMART,
649+
tool_outputs_summarized=tool_summarized_count,
650+
tokens_saved_by_summarization=tokens_saved_by_summarization,
651+
)
652+
else:
653+
result = messages
654+
655+
# Step 2: Prune tool outputs (fallback truncation)
656+
result, prune_result = self._prune.optimize(result, target_tokens, ledger)
657+
tokens_saved_by_truncation = prune_result.tokens_saved
523658

524659
if estimate_messages_tokens(result) <= target_tokens:
525660
return result, OptimizationResult(
526661
original_tokens=original_tokens,
527662
optimized_tokens=prune_result.optimized_tokens,
528663
tokens_saved=original_tokens - prune_result.optimized_tokens,
529664
strategy_used=OptimizerStrategy.SMART,
665+
tool_outputs_summarized=tool_summarized_count,
530666
tool_outputs_pruned=prune_result.tool_outputs_pruned,
667+
tokens_saved_by_summarization=tokens_saved_by_summarization,
668+
tokens_saved_by_truncation=tokens_saved_by_truncation,
531669
)
532670

533-
# Step 2: Sliding window
671+
# Step 3: Sliding window
534672
result, window_result = self._window.optimize(result, target_tokens, ledger)
535673

536674
if estimate_messages_tokens(result) <= target_tokens:
@@ -543,7 +681,7 @@ def optimize(
543681
messages_removed=window_result.messages_removed,
544682
)
545683

546-
# Step 3: Summarize
684+
# Step 4: Summarize conversation
547685
result, summary_result = self._summarize.optimize(result, target_tokens, ledger)
548686

549687
optimized_tokens = estimate_messages_tokens(result)
@@ -553,7 +691,10 @@ def optimize(
553691
optimized_tokens=optimized_tokens,
554692
tokens_saved=original_tokens - optimized_tokens,
555693
strategy_used=OptimizerStrategy.SMART,
694+
tool_outputs_summarized=tool_summarized_count,
556695
tool_outputs_pruned=prune_result.tool_outputs_pruned,
696+
tokens_saved_by_summarization=tokens_saved_by_summarization,
697+
tokens_saved_by_truncation=tokens_saved_by_truncation,
557698
messages_removed=window_result.messages_removed + summary_result.messages_removed,
558699
summary_added=summary_result.summary_added,
559700
)

0 commit comments

Comments
 (0)