Skip to content

Commit 91aaa9a

Browse files
committed
Release v4.5.7
1 parent 2b54eb5 commit 91aaa9a

File tree

23 files changed

+1744
-121
lines changed

23 files changed

+1744
-121
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>=4.5.6" \
19+
"praisonai>=4.5.7" \
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>=4.5.6" \
23+
"praisonai>=4.5.7" \
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>=4.5.6" \
19+
"praisonai>=4.5.7" \
2020
"praisonai[ui]" \
2121
"praisonai[crewai]"
2222

src/praisonai-agents/praisonaiagents/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,11 @@ def _get_lazy_cache():
338338
'HooksConfig': ('praisonaiagents.config.feature_configs', 'HooksConfig'),
339339
'SkillsConfig': ('praisonaiagents.config.feature_configs', 'SkillsConfig'),
340340
'AutonomyConfig': ('praisonaiagents.agent.autonomy', 'AutonomyConfig'),
341+
'EscalationStage': ('praisonaiagents.escalation.types', 'EscalationStage'),
342+
'EscalationPipeline': ('praisonaiagents.escalation.pipeline', 'EscalationPipeline'),
343+
'ObservabilityHooks': ('praisonaiagents.escalation.observability', 'ObservabilityHooks'),
344+
'ObservabilityEventType': ('praisonaiagents.escalation.observability', 'EventType'),
345+
'DoomLoopDetector': ('praisonaiagents.escalation.doom_loop', 'DoomLoopDetector'),
341346
'MemoryBackend': ('praisonaiagents.config.feature_configs', 'MemoryBackend'),
342347
'ChunkingStrategy': ('praisonaiagents.config.feature_configs', 'ChunkingStrategy'),
343348
'GuardrailAction': ('praisonaiagents.config.feature_configs', 'GuardrailAction'),

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

Lines changed: 160 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2033,6 +2033,64 @@ def _reset_doom_loop(self) -> None:
20332033
if self._doom_loop_tracker is not None:
20342034
self._doom_loop_tracker.reset()
20352035

2036+
@staticmethod
2037+
def _is_completion_signal(response_text: str) -> bool:
2038+
"""Check if response contains a completion signal using word boundaries.
2039+
2040+
Uses regex word boundaries to avoid false positives like
2041+
'abandoned' matching 'done' or 'unfinished' matching 'finished'.
2042+
2043+
Args:
2044+
response_text: The agent response to check
2045+
2046+
Returns:
2047+
True if a completion signal is detected
2048+
"""
2049+
import re
2050+
response_lower = response_text.lower()
2051+
# Negation patterns that should NOT be treated as completion
2052+
_NEGATION_RE = re.compile(
2053+
r'\b(?:not|never|no longer|hardly|barely|isn\'t|aren\'t|wasn\'t|weren\'t|hasn\'t|haven\'t|hadn\'t|won\'t|wouldn\'t|can\'t|couldn\'t|shouldn\'t|don\'t|doesn\'t|didn\'t)\b'
2054+
r'.{0,20}' # up to 20 chars between negation and keyword
2055+
)
2056+
# Word-boundary patterns to avoid substring false positives
2057+
_COMPLETION_PATTERNS = [
2058+
(re.compile(r'\btask\s+completed?\b'), False), # no negation check needed
2059+
(re.compile(r'\bcompleted\s+successfully\b'), False),
2060+
(re.compile(r'\ball\s+done\b'), False),
2061+
(re.compile(r'\bdone\b'), True), # 'done' needs negation check
2062+
(re.compile(r'\bfinished\b'), True), # 'finished' needs negation check
2063+
]
2064+
for pattern, needs_negation_check in _COMPLETION_PATTERNS:
2065+
match = pattern.search(response_lower)
2066+
if match:
2067+
if needs_negation_check:
2068+
# Check if a negation word precedes the match within 30 chars
2069+
start = max(0, match.start() - 30)
2070+
prefix = response_lower[start:match.start()]
2071+
if _NEGATION_RE.search(prefix):
2072+
continue # Skip — negated completion
2073+
# Also check "not X yet" pattern
2074+
end = min(len(response_lower), match.end() + 10)
2075+
suffix = response_lower[match.end():end]
2076+
if 'yet' in suffix:
2077+
neg_prefix = response_lower[max(0, match.start() - 15):match.start()]
2078+
if 'not' in neg_prefix:
2079+
continue
2080+
return True
2081+
return False
2082+
2083+
def _get_doom_recovery(self) -> str:
2084+
"""Get doom loop recovery action from tracker.
2085+
2086+
Returns:
2087+
Recovery action string: continue, retry_different, escalate_model,
2088+
request_help, or abort
2089+
"""
2090+
if self._doom_loop_tracker is None:
2091+
return "continue"
2092+
return self._doom_loop_tracker.get_recovery_action()
2093+
20362094
def _bridge_autonomy_level(self, level: str) -> None:
20372095
"""Bridge autonomy level to per-agent approval backend (G3/G-BRIDGE-1 fix).
20382096
@@ -2144,18 +2202,44 @@ def run_autonomous(
21442202
started_at=started_at,
21452203
)
21462204

2147-
# Check doom loop
2205+
# Check doom loop with graduated recovery (G-RECOVERY-2 fix)
21482206
if self._is_doom_loop():
2149-
return AutonomyResult(
2150-
success=False,
2151-
output="Task stopped due to repeated actions (doom loop)",
2152-
completion_reason="doom_loop",
2153-
iterations=iterations,
2154-
stage=stage,
2155-
actions=actions_taken,
2156-
duration_seconds=time_module.time() - start_time,
2157-
started_at=started_at,
2158-
)
2207+
recovery = self._get_doom_recovery()
2208+
obs = getattr(self, '_observability_hooks', None)
2209+
if obs is not None:
2210+
from ..escalation.observability import EventType as _EvtType
2211+
obs.emit(_EvtType.STEP_END, {
2212+
"doom_loop": True,
2213+
"recovery_action": recovery,
2214+
"iteration": iterations,
2215+
})
2216+
if recovery == "retry_different":
2217+
prompt = prompt + "\n\n[System: Previous approach repeated. Try a completely different strategy.]"
2218+
if self._doom_loop_tracker is not None:
2219+
self._doom_loop_tracker.clear_actions()
2220+
continue
2221+
elif recovery == "request_help":
2222+
return AutonomyResult(
2223+
success=False,
2224+
output="Task needs human guidance (doom loop recovery exhausted)",
2225+
completion_reason="needs_help",
2226+
iterations=iterations,
2227+
stage=stage,
2228+
actions=actions_taken,
2229+
duration_seconds=time_module.time() - start_time,
2230+
started_at=started_at,
2231+
)
2232+
else:
2233+
return AutonomyResult(
2234+
success=False,
2235+
output="Task stopped due to repeated actions (doom loop)",
2236+
completion_reason="doom_loop",
2237+
iterations=iterations,
2238+
stage=stage,
2239+
actions=actions_taken,
2240+
duration_seconds=time_module.time() - start_time,
2241+
started_at=started_at,
2242+
)
21592243

21602244
# Execute one turn using the agent's chat method
21612245
# Always use the original prompt (prompt re-injection)
@@ -2233,14 +2317,8 @@ def run_autonomous(
22332317
started_at=started_at,
22342318
)
22352319

2236-
# Check for keyword-based completion signals (fallback)
2237-
response_lower = response_str.lower()
2238-
completion_signals = [
2239-
"task completed", "task complete", "done",
2240-
"finished", "completed successfully",
2241-
]
2242-
2243-
if any(signal in response_lower for signal in completion_signals):
2320+
# Check for keyword-based completion signals (word-boundary, G-COMPLETION-1 fix)
2321+
if self._is_completion_signal(response_str):
22442322
return AutonomyResult(
22452323
success=True,
22462324
output=response_str,
@@ -2280,7 +2358,18 @@ def run_autonomous(
22802358
duration_seconds=time_module.time() - start_time,
22812359
started_at=started_at,
22822360
)
2283-
2361+
2362+
except KeyboardInterrupt:
2363+
return AutonomyResult(
2364+
success=False,
2365+
output="Task cancelled by user",
2366+
completion_reason="cancelled",
2367+
iterations=iterations,
2368+
stage=stage,
2369+
actions=actions_taken,
2370+
duration_seconds=time_module.time() - start_time,
2371+
started_at=started_at,
2372+
)
22842373
except Exception as e:
22852374
return AutonomyResult(
22862375
success=False,
@@ -2396,18 +2485,44 @@ async def main():
23962485
started_at=started_at,
23972486
)
23982487

2399-
# Check doom loop
2488+
# Check doom loop with graduated recovery (G-RECOVERY-2 fix)
24002489
if self._is_doom_loop():
2401-
return AutonomyResult(
2402-
success=False,
2403-
output="Task stopped due to repeated actions (doom loop)",
2404-
completion_reason="doom_loop",
2405-
iterations=iterations,
2406-
stage=stage,
2407-
actions=actions_taken,
2408-
duration_seconds=time_module.time() - start_time,
2409-
started_at=started_at,
2410-
)
2490+
recovery = self._get_doom_recovery()
2491+
obs = getattr(self, '_observability_hooks', None)
2492+
if obs is not None:
2493+
from ..escalation.observability import EventType as _EvtType
2494+
obs.emit(_EvtType.STEP_END, {
2495+
"doom_loop": True,
2496+
"recovery_action": recovery,
2497+
"iteration": iterations,
2498+
})
2499+
if recovery == "retry_different":
2500+
prompt = prompt + "\n\n[System: Previous approach repeated. Try a completely different strategy.]"
2501+
if self._doom_loop_tracker is not None:
2502+
self._doom_loop_tracker.clear_actions()
2503+
continue
2504+
elif recovery == "request_help":
2505+
return AutonomyResult(
2506+
success=False,
2507+
output="Task needs human guidance (doom loop recovery exhausted)",
2508+
completion_reason="needs_help",
2509+
iterations=iterations,
2510+
stage=stage,
2511+
actions=actions_taken,
2512+
duration_seconds=time_module.time() - start_time,
2513+
started_at=started_at,
2514+
)
2515+
else:
2516+
return AutonomyResult(
2517+
success=False,
2518+
output="Task stopped due to repeated actions (doom loop)",
2519+
completion_reason="doom_loop",
2520+
iterations=iterations,
2521+
stage=stage,
2522+
actions=actions_taken,
2523+
duration_seconds=time_module.time() - start_time,
2524+
started_at=started_at,
2525+
)
24112526

24122527
# Execute one turn using the agent's async chat method
24132528
# Always use the original prompt (prompt re-injection)
@@ -2485,14 +2600,8 @@ async def main():
24852600
started_at=started_at,
24862601
)
24872602

2488-
# Check for keyword-based completion signals (fallback)
2489-
response_lower = response_str.lower()
2490-
completion_signals = [
2491-
"task completed", "task complete", "done",
2492-
"finished", "completed successfully",
2493-
]
2494-
2495-
if any(signal in response_lower for signal in completion_signals):
2603+
# Check for keyword-based completion signals (word-boundary, G-COMPLETION-1 fix)
2604+
if self._is_completion_signal(response_str):
24962605
return AutonomyResult(
24972606
success=True,
24982607
output=response_str,
@@ -2538,7 +2647,18 @@ async def main():
25382647
duration_seconds=time_module.time() - start_time,
25392648
started_at=started_at,
25402649
)
2541-
2650+
2651+
except (KeyboardInterrupt, asyncio.CancelledError):
2652+
return AutonomyResult(
2653+
success=False,
2654+
output="Task cancelled",
2655+
completion_reason="cancelled",
2656+
iterations=iterations,
2657+
stage=stage,
2658+
actions=actions_taken,
2659+
duration_seconds=time_module.time() - start_time,
2660+
started_at=started_at,
2661+
)
25422662
except Exception as e:
25432663
return AutonomyResult(
25442664
success=False,

0 commit comments

Comments
 (0)