| """ |
| Trace Environment Implementation. |
| |
| An RL environment for Gmail financial transaction intelligence. |
| Wraps the internal TraceEnv with the OpenEnv Environment interface. |
| """ |
|
|
| import os |
| import sys |
| import yaml |
| import logging |
| from uuid import uuid4 |
|
|
| from openenv.core.env_server.interfaces import Environment |
| from openenv.core.env_server.types import State |
|
|
| logger = logging.getLogger(__name__) |
|
|
| |
| _PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) |
| if _PROJECT_ROOT not in sys.path: |
| sys.path.insert(0, _PROJECT_ROOT) |
|
|
| try: |
| from ..models import TraceAction, TraceObservation |
| except (ImportError, SystemError): |
| from models import TraceAction, TraceObservation |
|
|
| |
| from environments.trace_env.core.env import TraceEnv |
| from environments.trace_env.core.schemas import TraceAction as InternalAction |
|
|
|
|
| class TraceEnvironment(Environment): |
| """ |
| OpenEnv-compatible wrapper around the Trace internal environment. |
| |
| The environment fetches real financial transaction data from Gmail, |
| parses PDF/HTML attachments, and syncs to Google Sheets. |
| |
| Example: |
| >>> env = TraceEnvironment() |
| >>> obs = env.reset() |
| >>> print(obs.instruction) |
| >>> |
| >>> obs = env.step(TraceAction(action_type="RETRIEVE", content="financial emails", source="gmail")) |
| >>> print(obs.context) |
| """ |
|
|
| |
| SUPPORTS_CONCURRENT_SESSIONS: bool = False |
|
|
| def __init__(self): |
| """Initialize the Trace environment with config.""" |
| config_path = os.path.join(_PROJECT_ROOT, "configs", "env_config.yaml") |
| try: |
| with open(config_path) as f: |
| self._config = yaml.safe_load(f) or {} |
| except FileNotFoundError: |
| logger.warning(f"Config not found at {config_path}, using defaults") |
| self._config = {} |
|
|
| self._env = TraceEnv(self._config) |
| self._state = State(episode_id=str(uuid4()), step_count=0) |
|
|
| def reset(self) -> TraceObservation: |
| """ |
| Reset the environment and start a new episode. |
| |
| Returns: |
| TraceObservation with initial state |
| """ |
| task = { |
| "instruction": "Audit my financial transactions from Gmail.", |
| "difficulty": "medium", |
| "available_sources": ["gmail", "image"], |
| "ground_truth": {}, |
| } |
| obs = self._env.reset(task) |
| self._state = State(episode_id=obs.episode_id, step_count=0) |
|
|
| return TraceObservation( |
| episode_id=obs.episode_id, |
| step=obs.step, |
| instruction=obs.instruction, |
| available_sources=obs.available_sources, |
| context=obs.context, |
| memory_summary=obs.memory_summary, |
| world_state=obs.world_state, |
| done=False, |
| reward=0.0, |
| ) |
|
|
| def step(self, action: TraceAction) -> TraceObservation: |
| """ |
| Execute one agent action in the environment. |
| |
| Args: |
| action: TraceAction containing action_type, content, source |
| |
| Returns: |
| TraceObservation with results |
| """ |
| |
| internal_action = InternalAction( |
| action_type=action.action_type, |
| content=action.content, |
| source=action.source, |
| metadata=action.metadata, |
| ) |
|
|
| obs, reward, done, info = self._env.step(internal_action) |
| self._state.step_count += 1 |
|
|
| return TraceObservation( |
| episode_id=obs.episode_id, |
| step=obs.step, |
| instruction=obs.instruction, |
| available_sources=obs.available_sources, |
| context=obs.context, |
| memory_summary=obs.memory_summary, |
| world_state=obs.world_state, |
| done=done, |
| reward=reward, |
| metadata=info, |
| ) |
|
|
| @property |
| def state(self) -> State: |
| """ |
| Get the current environment state. |
| |
| Returns: |
| Current State with episode_id and step_count |
| """ |
| return self._state |
|
|