AUXteam's picture
Upload folder using huggingface_hub
1397957 verified
raw
history blame
6.55 kB
"""
Session processor for managing agentic loop execution.
"""
from typing import Optional, Dict, Any, AsyncIterator, List
from pydantic import BaseModel
from datetime import datetime
import asyncio
from ..provider.provider import StreamChunk
class DoomLoopDetector:
"""๋™์ผ ๋„๊ตฌ + ๋™์ผ ์ธ์ž ์—ฐ์† ํ˜ธ์ถœ์„ ๊ฐ์ง€ํ•˜์—ฌ ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€
์›๋ณธ opencode์™€ ๋™์ผํ•˜๊ฒŒ ๋„๊ตฌ ์ด๋ฆ„๊ณผ ์ธ์ž๋ฅผ ๋ชจ๋‘ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค.
๊ฐ™์€ ๋„๊ตฌ๋ผ๋„ ์ธ์ž๊ฐ€ ๋‹ค๋ฅด๋ฉด ์ •์ƒ์ ์ธ ๋ฐ˜๋ณต์œผ๋กœ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.
"""
def __init__(self, threshold: int = 3):
self.threshold = threshold
self.history: List[tuple[str, str]] = [] # (tool_name, args_hash)
def record(self, tool_name: str, args: Optional[Dict[str, Any]] = None) -> bool:
"""๋„๊ตฌ ํ˜ธ์ถœ์„ ๊ธฐ๋กํ•˜๊ณ  doom loop ๊ฐ์ง€ ์‹œ True ๋ฐ˜ํ™˜
Args:
tool_name: ๋„๊ตฌ ์ด๋ฆ„
args: ๋„๊ตฌ ์ธ์ž (์—†์œผ๋ฉด ๋นˆ dict๋กœ ์ฒ˜๋ฆฌ)
Returns:
True if doom loop detected, False otherwise
"""
import json
import hashlib
# ์ธ์ž๋ฅผ ์ •๊ทœํ™”ํ•˜์—ฌ ํ•ด์‹œ ์ƒ์„ฑ (์›๋ณธ์ฒ˜๋Ÿผ JSON ๋น„๊ต)
args_dict = args or {}
args_str = json.dumps(args_dict, sort_keys=True, default=str)
args_hash = hashlib.md5(args_str.encode()).hexdigest()[:8]
call_signature = (tool_name, args_hash)
self.history.append(call_signature)
# ์ตœ๊ทผ threshold๊ฐœ๊ฐ€ ๋ชจ๋‘ ๊ฐ™์€ (๋„๊ตฌ + ์ธ์ž)์ธ์ง€ ํ™•์ธ
if len(self.history) >= self.threshold:
recent = self.history[-self.threshold:]
if len(set(recent)) == 1: # ํŠœํ”Œ ๋น„๊ต (๋„๊ตฌ+์ธ์ž)
return True
return False
def reset(self):
self.history = []
class RetryConfig(BaseModel):
"""์žฌ์‹œ๋„ ์„ค์ •"""
max_retries: int = 3
base_delay: float = 1.0 # seconds
max_delay: float = 30.0
exponential_base: float = 2.0
class StepInfo(BaseModel):
"""์Šคํ… ์ •๋ณด"""
step: int
started_at: datetime
finished_at: Optional[datetime] = None
tool_calls: List[str] = []
status: str = "running" # running, completed, error, doom_loop
class SessionProcessor:
"""
Agentic loop ์‹คํ–‰์„ ๊ด€๋ฆฌํ•˜๋Š” ํ”„๋กœ์„ธ์„œ.
Features:
- Doom loop ๋ฐฉ์ง€ (๋™์ผ ๋„๊ตฌ ์—ฐ์† ํ˜ธ์ถœ ๊ฐ์ง€)
- ์ž๋™ ์žฌ์‹œ๋„ (exponential backoff)
- ์Šคํ… ์ถ”์  (step-start, step-finish ์ด๋ฒคํŠธ)
"""
_processors: Dict[str, "SessionProcessor"] = {}
def __init__(self, session_id: str, max_steps: int = 50, doom_threshold: int = 3):
self.session_id = session_id
self.max_steps = max_steps
self.doom_detector = DoomLoopDetector(threshold=doom_threshold)
self.retry_config = RetryConfig()
self.steps: List[StepInfo] = []
self.current_step: Optional[StepInfo] = None
self.aborted = False
@classmethod
def get_or_create(cls, session_id: str, **kwargs) -> "SessionProcessor":
if session_id not in cls._processors:
cls._processors[session_id] = cls(session_id, **kwargs)
return cls._processors[session_id]
@classmethod
def remove(cls, session_id: str) -> None:
if session_id in cls._processors:
del cls._processors[session_id]
def start_step(self) -> StepInfo:
"""์ƒˆ ์Šคํ… ์‹œ์ž‘"""
step_num = len(self.steps) + 1
self.current_step = StepInfo(
step=step_num,
started_at=datetime.utcnow()
)
self.steps.append(self.current_step)
return self.current_step
def finish_step(self, status: str = "completed") -> StepInfo:
"""ํ˜„์žฌ ์Šคํ… ์™„๋ฃŒ"""
if self.current_step:
self.current_step.finished_at = datetime.utcnow()
self.current_step.status = status
return self.current_step
def record_tool_call(self, tool_name: str, tool_args: Optional[Dict[str, Any]] = None) -> bool:
"""๋„๊ตฌ ํ˜ธ์ถœ ๊ธฐ๋ก, doom loop ๊ฐ์ง€ ์‹œ True ๋ฐ˜ํ™˜
Args:
tool_name: ๋„๊ตฌ ์ด๋ฆ„
tool_args: ๋„๊ตฌ ์ธ์ž (doom loop ํŒ๋ณ„์— ์‚ฌ์šฉ)
Returns:
True if doom loop detected, False otherwise
"""
if self.current_step:
self.current_step.tool_calls.append(tool_name)
return self.doom_detector.record(tool_name, tool_args)
def is_doom_loop(self) -> bool:
"""ํ˜„์žฌ doom loop ์ƒํƒœ์ธ์ง€ ํ™•์ธ"""
return len(self.doom_detector.history) >= self.doom_detector.threshold and \
len(set(self.doom_detector.history[-self.doom_detector.threshold:])) == 1
def should_continue(self) -> bool:
"""๋ฃจํ”„ ๊ณ„์† ์—ฌ๋ถ€"""
if self.aborted:
return False
if len(self.steps) >= self.max_steps:
return False
if self.is_doom_loop():
return False
return True
def abort(self) -> None:
"""ํ”„๋กœ์„ธ์„œ ์ค‘๋‹จ"""
self.aborted = True
async def calculate_retry_delay(self, attempt: int) -> float:
"""exponential backoff ๋”œ๋ ˆ์ด ๊ณ„์‚ฐ"""
delay = self.retry_config.base_delay * (self.retry_config.exponential_base ** attempt)
return min(delay, self.retry_config.max_delay)
async def retry_with_backoff(self, func, *args, **kwargs):
"""exponential backoff์œผ๋กœ ํ•จ์ˆ˜ ์žฌ์‹œ๋„"""
last_error = None
for attempt in range(self.retry_config.max_retries):
try:
return await func(*args, **kwargs)
except Exception as e:
last_error = e
if attempt < self.retry_config.max_retries - 1:
delay = await self.calculate_retry_delay(attempt)
await asyncio.sleep(delay)
raise last_error
def get_summary(self) -> Dict[str, Any]:
"""ํ”„๋กœ์„ธ์„œ ์ƒํƒœ ์š”์•ฝ"""
return {
"session_id": self.session_id,
"total_steps": len(self.steps),
"max_steps": self.max_steps,
"aborted": self.aborted,
"doom_loop_detected": self.is_doom_loop(),
"steps": [
{
"step": s.step,
"status": s.status,
"tool_calls": s.tool_calls,
"duration": (s.finished_at - s.started_at).total_seconds() if s.finished_at else None
}
for s in self.steps
]
}