Files
2026-07-13 13:32:05 +08:00

252 lines
7.9 KiB
Python

import asyncio
import inspect
from typing import Awaitable, Callable, List, Optional
from pydantic import BaseModel
from rich.progress import Progress
from deepeval.dataset import ConversationalGolden
from deepeval.simulator.controller.template import SimulatorControllerTemplate
from deepeval.simulator.controller.types import Context, Decision
from deepeval.simulator.schema import ConversationCompletion
from deepeval.test_case import Turn
from deepeval.utils import update_pbar, serialize_to_json
def proceed() -> Decision:
return Decision(should_end=False)
def end(reason: Optional[str] = None) -> Decision:
return Decision(should_end=True, reason=reason)
class SimulationController:
def __init__(
self,
generate_schema: Callable[[str, BaseModel], BaseModel],
a_generate_schema: Callable[[str, BaseModel], Awaitable[BaseModel]],
controller: Callable,
):
self.controller = controller
self.template = SimulatorControllerTemplate
self.generate_schema = generate_schema
self.a_generate_schema = a_generate_schema
def run(
self,
turns: List[Turn],
golden: ConversationalGolden,
index: int,
thread_id: str,
simulation_counter: int,
max_user_simulations: int,
progress: Optional[Progress] = None,
pbar_turns_id: Optional[int] = None,
) -> bool:
if self.controller is expected_outcome_controller:
return self.controller.run(
self, turns, golden, progress, pbar_turns_id
)
ctx = self._build_context(
turns=turns,
golden=golden,
index=index,
thread_id=thread_id,
simulation_counter=simulation_counter,
max_user_simulations=max_user_simulations,
)
decision = self._invoke_controller(ctx)
if inspect.isawaitable(decision):
decision = asyncio.run(decision)
return self._should_end(decision, progress, pbar_turns_id)
async def a_run(
self,
turns: List[Turn],
golden: ConversationalGolden,
index: int,
thread_id: str,
simulation_counter: int,
max_user_simulations: int,
progress: Optional[Progress] = None,
pbar_turns_id: Optional[int] = None,
) -> bool:
if self.controller is expected_outcome_controller:
return await self.controller.a_run(
self, turns, golden, progress, pbar_turns_id
)
ctx = self._build_context(
turns=turns,
golden=golden,
index=index,
thread_id=thread_id,
simulation_counter=simulation_counter,
max_user_simulations=max_user_simulations,
)
decision = self._invoke_controller(ctx)
if inspect.isawaitable(decision):
decision = await decision
return self._should_end(decision, progress, pbar_turns_id)
def check_expected_outcome(
self,
turns: List[Turn],
golden: ConversationalGolden,
progress: Optional[Progress] = None,
pbar_turns_id: Optional[int] = None,
) -> bool:
if golden.expected_outcome is None:
return False
conversation_history = serialize_to_json(
turns, indent=4, ensure_ascii=False
)
prompt = self.template.check_expected_outcome(
conversation_history, golden.expected_outcome
)
is_complete: ConversationCompletion = self._generate_schema(
prompt, ConversationCompletion
)
if is_complete.is_complete:
update_pbar(
progress,
pbar_turns_id,
advance_to_end=is_complete.is_complete,
)
return is_complete.is_complete
async def a_check_expected_outcome(
self,
turns: List[Turn],
golden: ConversationalGolden,
progress: Optional[Progress] = None,
pbar_turns_id: Optional[int] = None,
) -> bool:
if golden.expected_outcome is None:
return False
conversation_history = serialize_to_json(
turns, indent=4, ensure_ascii=False
)
prompt = self.template.check_expected_outcome(
conversation_history, golden.expected_outcome
)
is_complete: ConversationCompletion = await self._a_generate_schema(
prompt, ConversationCompletion
)
if is_complete.is_complete:
update_pbar(
progress,
pbar_turns_id,
advance_to_end=is_complete.is_complete,
)
return is_complete.is_complete
def _build_context(
self,
turns: List[Turn],
golden: ConversationalGolden,
index: int,
thread_id: str,
simulation_counter: int,
max_user_simulations: int,
) -> Context:
last_user_turn = next(
(turn for turn in reversed(turns) if turn.role == "user"), None
)
last_assistant_turn = next(
(turn for turn in reversed(turns) if turn.role == "assistant"),
None,
)
return Context(
turns=list(turns),
golden=golden,
index=index,
thread_id=thread_id,
simulated_user_turns=simulation_counter,
max_user_simulations=max_user_simulations,
last_user_turn=last_user_turn,
last_assistant_turn=last_assistant_turn,
)
def _invoke_controller(self, ctx: Context):
controller_kwargs = {
"turns": ctx.turns,
"golden": ctx.golden,
"index": ctx.index,
"thread_id": ctx.thread_id,
"simulated_user_turns": ctx.simulated_user_turns,
"max_user_simulations": ctx.max_user_simulations,
"last_user_turn": ctx.last_user_turn,
"last_assistant_turn": ctx.last_assistant_turn,
}
supported_args = set(
inspect.signature(self.controller).parameters.keys()
)
return self.controller(
**{
key: value
for key, value in controller_kwargs.items()
if key in supported_args
}
)
def _normalize_decision(self, decision: Optional[Decision]) -> Decision:
if not isinstance(decision, Decision):
return Decision(should_end=False)
return decision
def _should_end(
self,
decision: Optional[Decision],
progress: Optional[Progress],
pbar_turns_id: Optional[int],
) -> bool:
should_end = self._normalize_decision(decision).should_end
if should_end:
update_pbar(progress, pbar_turns_id, advance_to_end=True)
return should_end
def _generate_schema(self, prompt: str, schema: BaseModel) -> BaseModel:
return self.generate_schema(prompt, schema)
async def _a_generate_schema(
self, prompt: str, schema: BaseModel
) -> BaseModel:
return await self.a_generate_schema(prompt, schema)
class _ExpectedOutcomeController:
def run(
self,
simulation_controller: SimulationController,
turns: List[Turn],
golden: ConversationalGolden,
progress: Optional[Progress] = None,
pbar_turns_id: Optional[int] = None,
) -> bool:
return simulation_controller.check_expected_outcome(
turns, golden, progress, pbar_turns_id
)
async def a_run(
self,
simulation_controller: SimulationController,
turns: List[Turn],
golden: ConversationalGolden,
progress: Optional[Progress] = None,
pbar_turns_id: Optional[int] = None,
) -> bool:
return await simulation_controller.a_check_expected_outcome(
turns, golden, progress, pbar_turns_id
)
expected_outcome_controller = _ExpectedOutcomeController()