File size: 4,636 Bytes
0d3f7cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""Episodic memory implementation."""

from __future__ import annotations

import json
import logging
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from hermes.config.settings import get_settings
from hermes.core.types import EpisodicMemory

logger = logging.getLogger(__name__)


class EpisodicMemoryStore:
    """Episodic memory for storing task experiences."""

    def __init__(self, storage_path: str | None = None) -> None:
        self.settings = get_settings()
        self._storage_path = Path(storage_path or "data/episodic_memory.json")
        self._episodes: list[EpisodicMemory] = []
        self._load()

    def _load(self) -> None:
        """Load episodes from disk."""
        try:
            if self._storage_path.exists():
                data = json.loads(self._storage_path.read_text(encoding="utf-8"))
                self._episodes = [EpisodicMemory(**ep) for ep in data]
        except Exception as e:
            logger.warning(f"Could not load episodic memory: {e}")
            self._episodes = []

    def _save(self) -> None:
        """Save episodes to disk."""
        try:
            self._storage_path.parent.mkdir(parents=True, exist_ok=True)
            data = [ep.model_dump() for ep in self._episodes]
            self._storage_path.write_text(
                json.dumps(data, indent=2, default=str), encoding="utf-8"
            )
        except Exception as e:
            logger.error(f"Could not save episodic memory: {e}")

    async def record_episode(

        self,

        task_id: str,

        task_description: str,

        actions: list[str],

        results: dict[str, Any],

        outcome: str = "",

        duration_seconds: float = 0.0,

    ) -> EpisodicMemory:
        """Record a new episode."""
        episode = EpisodicMemory(
            task_id=task_id,
            task_description=task_description,
            actions=actions,
            results=results,
            outcome=outcome,
            duration_seconds=duration_seconds,
            timestamp=datetime.now(UTC),
        )
        self._episodes.append(episode)
        self._save()
        return episode

    async def retrieve(

        self,

        query: str | None = None,

        task_id: str | None = None,

        limit: int = 10,

    ) -> list[EpisodicMemory]:
        """Retrieve episodes matching criteria."""
        results = self._episodes

        if task_id:
            results = [ep for ep in results if ep.task_id == task_id]

        if query:
            query_lower = query.lower()
            results = [
                ep for ep in results
                if query_lower in ep.task_description.lower()
                or any(query_lower in action.lower() for action in ep.actions)
            ]

        return sorted(results, key=lambda e: e.timestamp, reverse=True)[:limit]

    async def get_similar_episodes(

        self, task_description: str, limit: int = 5

    ) -> list[EpisodicMemory]:
        """Get episodes similar to a task description."""
        words = set(task_description.lower().split())

        scored: list[tuple[EpisodicMemory, float]] = []
        for ep in self._episodes:
            ep_words = set(ep.task_description.lower().split())
            overlap = len(words & ep_words)
            total = len(words | ep_words)
            score = overlap / total if total > 0 else 0.0
            scored.append((ep, score))

        scored.sort(key=lambda x: x[1], reverse=True)
        return [ep for ep, _ in scored[:limit] if _ > 0]

    async def get_statistics(self) -> dict[str, Any]:
        """Get episodic memory statistics."""
        if not self._episodes:
            return {"total_episodes": 0}

        durations = [ep.duration_seconds for ep in self._episodes]
        outcomes: dict[str, int] = {}
        for ep in self._episodes:
            outcome = ep.outcome or "unknown"
            outcomes[outcome] = outcomes.get(outcome, 0) + 1

        return {
            "total_episodes": len(self._episodes),
            "avg_duration_seconds": sum(durations) / len(durations) if durations else 0,
            "outcomes": outcomes,
            "date_range": {
                "earliest": min(ep.timestamp for ep in self._episodes).isoformat(),
                "latest": max(ep.timestamp for ep in self._episodes).isoformat(),
            },
        }

    async def clear(self) -> None:
        """Clear all episodes."""
        self._episodes.clear()
        self._save()