Spaces:
Paused
Paused
| """ | |
| 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 | |
| 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] | |
| 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 | |
| ] | |
| } | |